From df3ab7657c1f32d105424ffdabbf5f917678f123 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 16 Jul 2025 17:10:40 -0400 Subject: [PATCH 001/159] program: make lp shares reduce only --- programs/drift/src/instructions/user.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 27a2cf18d0..3717db74cc 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2937,6 +2937,9 @@ pub fn handle_add_perp_lp_shares<'c: 'info, 'info>( let clock = Clock::get()?; let now = clock.unix_timestamp; + msg!("add_perp_lp_shares is disabled"); + return Err(ErrorCode::DefaultError.into()); + let AccountMaps { perp_market_map, spot_market_map, @@ -3054,9 +3057,10 @@ pub fn handle_remove_perp_lp_shares_in_expiring_market<'c: 'info, 'info>( // additional validate { + let signer_is_admin = ctx.accounts.signer.key() == admin_hot_wallet::id(); let market = perp_market_map.get_ref(&market_index)?; validate!( - market.is_reduce_only()?, + market.is_reduce_only()? || signer_is_admin, ErrorCode::PerpMarketNotInReduceOnly, "Can only permissionless burn when market is in reduce only" )?; @@ -4630,6 +4634,7 @@ pub struct RemoveLiquidityInExpiredMarket<'info> { pub state: Box>, #[account(mut)] pub user: AccountLoader<'info, User>, + pub signer: Signer<'info>, } #[derive(Accounts)] From fed9dc6c8a3abe194b4179ee65524d57acc38111 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 11:01:16 -0400 Subject: [PATCH 002/159] init --- programs/drift/src/controller/liquidation.rs | 63 +- programs/drift/src/controller/lp.rs | 401 ---- programs/drift/src/controller/lp/tests.rs | 1818 ----------------- programs/drift/src/controller/mod.rs | 1 - programs/drift/src/controller/orders.rs | 152 +- programs/drift/src/controller/pnl.rs | 54 +- programs/drift/src/controller/position.rs | 108 +- .../drift/src/controller/position/tests.rs | 605 +----- programs/drift/src/instructions/admin.rs | 33 - programs/drift/src/instructions/keeper.rs | 31 - programs/drift/src/instructions/user.rs | 226 +- programs/drift/src/lib.rs | 38 - programs/drift/src/math/bankruptcy.rs | 1 - programs/drift/src/math/cp_curve/tests.rs | 177 +- programs/drift/src/math/lp.rs | 191 -- programs/drift/src/math/lp/tests.rs | 451 ---- programs/drift/src/math/margin.rs | 14 +- programs/drift/src/math/margin/tests.rs | 148 -- programs/drift/src/math/mod.rs | 1 - programs/drift/src/math/orders.rs | 1 - programs/drift/src/math/position.rs | 68 +- programs/drift/src/state/perp_market.rs | 23 - programs/drift/src/state/user.rs | 118 +- programs/drift/src/validation/position.rs | 8 - 24 files changed, 46 insertions(+), 4685 deletions(-) delete mode 100644 programs/drift/src/controller/lp.rs delete mode 100644 programs/drift/src/controller/lp/tests.rs delete mode 100644 programs/drift/src/math/lp.rs delete mode 100644 programs/drift/src/math/lp/tests.rs diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index d470c4757b..137bc1d055 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -5,7 +5,6 @@ use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; use crate::controller::funding::settle_funding_payment; -use crate::controller::lp::burn_lp_shares; use crate::controller::orders; use crate::controller::orders::{cancel_order, fill_perp_order, place_perp_order}; use crate::controller::position::{ @@ -181,8 +180,7 @@ pub fn liquidate_perp( let position_index = get_position_index(&user.perp_positions, market_index)?; validate!( user.perp_positions[position_index].is_open_position() - || user.perp_positions[position_index].has_open_order() - || user.perp_positions[position_index].is_lp(), + || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; @@ -222,27 +220,7 @@ pub fn liquidate_perp( drop(market); // burning lp shares = removing open bids/asks - let lp_shares = user.perp_positions[position_index].lp_shares; - if lp_shares > 0 { - let (position_delta, pnl) = burn_lp_shares( - &mut user.perp_positions[position_index], - perp_market_map.get_ref_mut(&market_index)?.deref_mut(), - lp_shares, - oracle_price, - )?; - - // emit LP record for shares removed - emit_stack::<_, { LPRecord::SIZE }>(LPRecord { - ts: now, - action: LPAction::RemoveLiquidity, - user: *user_key, - n_shares: lp_shares, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - })?; - } + let lp_shares = 0; // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { @@ -824,8 +802,7 @@ pub fn liquidate_perp_with_fill( let position_index = get_position_index(&user.perp_positions, market_index)?; validate!( user.perp_positions[position_index].is_open_position() - || user.perp_positions[position_index].has_open_order() - || user.perp_positions[position_index].is_lp(), + || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; @@ -865,27 +842,7 @@ pub fn liquidate_perp_with_fill( drop(market); // burning lp shares = removing open bids/asks - let lp_shares = user.perp_positions[position_index].lp_shares; - if lp_shares > 0 { - let (position_delta, pnl) = burn_lp_shares( - &mut user.perp_positions[position_index], - perp_market_map.get_ref_mut(&market_index)?.deref_mut(), - lp_shares, - oracle_price, - )?; - - // emit LP record for shares removed - emit_stack::<_, { LPRecord::SIZE }>(LPRecord { - ts: now, - action: LPAction::RemoveLiquidity, - user: *user_key, - n_shares: lp_shares, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - })?; - } + let lp_shares = 0; // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { @@ -2435,12 +2392,6 @@ pub fn liquidate_borrow_for_perp_pnl( base_asset_amount )?; - validate!( - !user_position.is_lp(), - ErrorCode::InvalidPerpPositionToLiquidate, - "user is an lp. must call liquidate_perp first" - )?; - let pnl = user_position.quote_asset_amount.cast::()?; validate!( @@ -2970,12 +2921,6 @@ pub fn liquidate_perp_pnl_for_deposit( base_asset_amount )?; - validate!( - !user_position.is_lp(), - ErrorCode::InvalidPerpPositionToLiquidate, - "user is an lp. must call liquidate_perp first" - )?; - let unsettled_pnl = user_position.quote_asset_amount.cast::()?; validate!( diff --git a/programs/drift/src/controller/lp.rs b/programs/drift/src/controller/lp.rs deleted file mode 100644 index e9e3f37e35..0000000000 --- a/programs/drift/src/controller/lp.rs +++ /dev/null @@ -1,401 +0,0 @@ -use anchor_lang::prelude::{msg, Pubkey}; - -use crate::bn::U192; -use crate::controller; -use crate::controller::position::update_position_and_market; -use crate::controller::position::{get_position_index, PositionDelta}; -use crate::emit; -use crate::error::{DriftResult, ErrorCode}; -use crate::get_struct_values; -use crate::math::casting::Cast; -use crate::math::cp_curve::{get_update_k_result, update_k}; -use crate::math::lp::calculate_settle_lp_metrics; -use crate::math::position::calculate_base_asset_value_with_oracle_price; -use crate::math::safe_math::SafeMath; - -use crate::state::events::{LPAction, LPRecord}; -use crate::state::oracle_map::OracleMap; -use crate::state::perp_market::PerpMarket; -use crate::state::perp_market_map::PerpMarketMap; -use crate::state::state::State; -use crate::state::user::PerpPosition; -use crate::state::user::User; -use crate::validate; -use anchor_lang::prelude::Account; - -#[cfg(test)] -mod tests; - -pub fn apply_lp_rebase_to_perp_market( - perp_market: &mut PerpMarket, - expo_diff: i8, -) -> DriftResult<()> { - // target_base_asset_amount_per_lp is the only one that it doesnt get applied - // thus changing the base of lp and without changing target_base_asset_amount_per_lp - // causes an implied change - - validate!(expo_diff != 0, ErrorCode::DefaultError, "expo_diff = 0")?; - - perp_market.amm.per_lp_base = perp_market.amm.per_lp_base.safe_add(expo_diff)?; - let rebase_divisor: i128 = 10_i128.pow(expo_diff.abs().cast()?); - - if expo_diff > 0 { - perp_market.amm.base_asset_amount_per_lp = perp_market - .amm - .base_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - - perp_market.amm.quote_asset_amount_per_lp = perp_market - .amm - .quote_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - - perp_market.amm.total_fee_earned_per_lp = perp_market - .amm - .total_fee_earned_per_lp - .safe_mul(rebase_divisor.cast()?)?; - } else { - perp_market.amm.base_asset_amount_per_lp = perp_market - .amm - .base_asset_amount_per_lp - .safe_div(rebase_divisor)?; - - perp_market.amm.quote_asset_amount_per_lp = perp_market - .amm - .quote_asset_amount_per_lp - .safe_div(rebase_divisor)?; - - perp_market.amm.total_fee_earned_per_lp = perp_market - .amm - .total_fee_earned_per_lp - .safe_div(rebase_divisor.cast()?)?; - } - - msg!( - "rebasing perp market_index={} per_lp_base expo_diff={}", - perp_market.market_index, - expo_diff, - ); - - crate::validation::perp_market::validate_perp_market(perp_market)?; - - Ok(()) -} - -pub fn apply_lp_rebase_to_perp_position( - perp_market: &PerpMarket, - perp_position: &mut PerpPosition, -) -> DriftResult<()> { - let expo_diff = perp_market - .amm - .per_lp_base - .safe_sub(perp_position.per_lp_base)?; - - if expo_diff > 0 { - let rebase_divisor: i64 = 10_i64.pow(expo_diff.cast()?); - - perp_position.last_base_asset_amount_per_lp = perp_position - .last_base_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - perp_position.last_quote_asset_amount_per_lp = perp_position - .last_quote_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - - msg!( - "rebasing perp position for market_index={} per_lp_base by expo_diff={}", - perp_market.market_index, - expo_diff, - ); - } else if expo_diff < 0 { - let rebase_divisor: i64 = 10_i64.pow(expo_diff.abs().cast()?); - - perp_position.last_base_asset_amount_per_lp = perp_position - .last_base_asset_amount_per_lp - .safe_div(rebase_divisor)?; - perp_position.last_quote_asset_amount_per_lp = perp_position - .last_quote_asset_amount_per_lp - .safe_div(rebase_divisor)?; - - msg!( - "rebasing perp position for market_index={} per_lp_base by expo_diff={}", - perp_market.market_index, - expo_diff, - ); - } - - perp_position.per_lp_base = perp_position.per_lp_base.safe_add(expo_diff)?; - - Ok(()) -} - -pub fn mint_lp_shares( - position: &mut PerpPosition, - market: &mut PerpMarket, - n_shares: u64, -) -> DriftResult<()> { - let amm = market.amm; - - let (sqrt_k,) = get_struct_values!(amm, sqrt_k); - - if position.lp_shares > 0 { - settle_lp_position(position, market)?; - } else { - position.last_base_asset_amount_per_lp = amm.base_asset_amount_per_lp.cast()?; - position.last_quote_asset_amount_per_lp = amm.quote_asset_amount_per_lp.cast()?; - position.per_lp_base = amm.per_lp_base; - } - - // add share balance - position.lp_shares = position.lp_shares.safe_add(n_shares)?; - - // update market state - let new_sqrt_k = sqrt_k.safe_add(n_shares.cast()?)?; - let new_sqrt_k_u192 = U192::from(new_sqrt_k); - - let update_k_result = get_update_k_result(market, new_sqrt_k_u192, true)?; - update_k(market, &update_k_result)?; - - market.amm.user_lp_shares = market.amm.user_lp_shares.safe_add(n_shares.cast()?)?; - - crate::validation::perp_market::validate_perp_market(market)?; - crate::validation::position::validate_perp_position_with_perp_market(position, market)?; - - Ok(()) -} - -pub fn settle_lp_position( - position: &mut PerpPosition, - market: &mut PerpMarket, -) -> DriftResult<(PositionDelta, i64)> { - if position.base_asset_amount > 0 { - validate!( - position.last_cumulative_funding_rate.cast::()? - == market.amm.cumulative_funding_rate_long, - ErrorCode::InvalidPerpPositionDetected - )?; - } else if position.base_asset_amount < 0 { - validate!( - position.last_cumulative_funding_rate.cast::()? - == market.amm.cumulative_funding_rate_short, - ErrorCode::InvalidPerpPositionDetected - )?; - } - - apply_lp_rebase_to_perp_position(market, position)?; - - let lp_metrics: crate::math::lp::LPMetrics = - calculate_settle_lp_metrics(&market.amm, position)?; - - let position_delta = PositionDelta { - base_asset_amount: lp_metrics.base_asset_amount.cast()?, - quote_asset_amount: lp_metrics.quote_asset_amount.cast()?, - remainder_base_asset_amount: Some(lp_metrics.remainder_base_asset_amount.cast::()?), - }; - - let pnl: i64 = update_position_and_market(position, market, &position_delta)?; - - position.last_base_asset_amount_per_lp = market.amm.base_asset_amount_per_lp.cast()?; - position.last_quote_asset_amount_per_lp = market.amm.quote_asset_amount_per_lp.cast()?; - - crate::validation::perp_market::validate_perp_market(market)?; - crate::validation::position::validate_perp_position_with_perp_market(position, market)?; - - Ok((position_delta, pnl)) -} - -pub fn settle_lp( - user: &mut User, - user_key: &Pubkey, - market: &mut PerpMarket, - now: i64, -) -> DriftResult { - if let Ok(position) = user.get_perp_position_mut(market.market_index) { - if position.lp_shares > 0 { - let (position_delta, pnl) = settle_lp_position(position, market)?; - - if position_delta.base_asset_amount != 0 || position_delta.quote_asset_amount != 0 { - crate::emit!(LPRecord { - ts: now, - action: LPAction::SettleLiquidity, - user: *user_key, - market_index: market.market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - n_shares: 0 - }); - } - } - } - - Ok(()) -} - -// note: must settle funding before settling the lp bc -// settling the lp can take on a new position which requires funding -// to be up-to-date -pub fn settle_funding_payment_then_lp( - user: &mut User, - user_key: &Pubkey, - market: &mut PerpMarket, - now: i64, -) -> DriftResult { - crate::controller::funding::settle_funding_payment(user, user_key, market, now)?; - settle_lp(user, user_key, market, now) -} - -pub fn burn_lp_shares( - position: &mut PerpPosition, - market: &mut PerpMarket, - shares_to_burn: u64, - oracle_price: i64, -) -> DriftResult<(PositionDelta, i64)> { - // settle - let (mut position_delta, mut pnl) = settle_lp_position(position, market)?; - - // clean up - let unsettled_remainder = market - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(position.remainder_base_asset_amount.cast()?)?; - if shares_to_burn as u128 == market.amm.user_lp_shares && unsettled_remainder != 0 { - crate::validate!( - unsettled_remainder.unsigned_abs() <= market.amm.order_step_size as u128, - ErrorCode::UnableToBurnLPTokens, - "unsettled baa on final burn too big rel to stepsize {}: {} (remainder:{})", - market.amm.order_step_size, - market.amm.base_asset_amount_with_unsettled_lp, - position.remainder_base_asset_amount - )?; - - // sub bc lps take the opposite side of the user - position.remainder_base_asset_amount = position - .remainder_base_asset_amount - .safe_sub(unsettled_remainder.cast()?)?; - } - - // update stats - if position.remainder_base_asset_amount != 0 { - let base_asset_amount = position.remainder_base_asset_amount as i128; - - // user closes the dust - market.amm.base_asset_amount_with_amm = market - .amm - .base_asset_amount_with_amm - .safe_sub(base_asset_amount)?; - - market.amm.base_asset_amount_with_unsettled_lp = market - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(base_asset_amount)?; - - let dust_base_asset_value = calculate_base_asset_value_with_oracle_price(base_asset_amount, oracle_price)? - .safe_add(1) // round up - ?; - - let dust_burn_position_delta = PositionDelta { - base_asset_amount: 0, - quote_asset_amount: -dust_base_asset_value.cast()?, - remainder_base_asset_amount: Some(-position.remainder_base_asset_amount.cast()?), - }; - - update_position_and_market(position, market, &dust_burn_position_delta)?; - - msg!( - "perp {} remainder_base_asset_amount burn fee= {}", - position.market_index, - dust_base_asset_value - ); - - position_delta.quote_asset_amount = position_delta - .quote_asset_amount - .safe_sub(dust_base_asset_value.cast()?)?; - pnl = pnl.safe_sub(dust_base_asset_value.cast()?)?; - } - - // update last_ metrics - position.last_base_asset_amount_per_lp = market.amm.base_asset_amount_per_lp.cast()?; - position.last_quote_asset_amount_per_lp = market.amm.quote_asset_amount_per_lp.cast()?; - - // burn shares - position.lp_shares = position.lp_shares.safe_sub(shares_to_burn)?; - - market.amm.user_lp_shares = market.amm.user_lp_shares.safe_sub(shares_to_burn.cast()?)?; - - // update market state - let new_sqrt_k = market.amm.sqrt_k.safe_sub(shares_to_burn.cast()?)?; - let new_sqrt_k_u192 = U192::from(new_sqrt_k); - - let update_k_result = get_update_k_result(market, new_sqrt_k_u192, false)?; - update_k(market, &update_k_result)?; - - crate::validation::perp_market::validate_perp_market(market)?; - crate::validation::position::validate_perp_position_with_perp_market(position, market)?; - - Ok((position_delta, pnl)) -} - -pub fn remove_perp_lp_shares( - perp_market_map: PerpMarketMap, - oracle_map: &mut OracleMap, - state: &Account, - user: &mut std::cell::RefMut, - user_key: Pubkey, - shares_to_burn: u64, - market_index: u16, - now: i64, -) -> DriftResult<()> { - let position_index = get_position_index(&user.perp_positions, market_index)?; - - // standardize n shares to burn - // account for issue where lp shares are smaller than step size - let shares_to_burn = if user.perp_positions[position_index].lp_shares == shares_to_burn { - shares_to_burn - } else { - let market = perp_market_map.get_ref(&market_index)?; - crate::math::orders::standardize_base_asset_amount( - shares_to_burn.cast()?, - market.amm.order_step_size, - )? - .cast()? - }; - - if shares_to_burn == 0 { - return Ok(()); - } - - let mut market = perp_market_map.get_ref_mut(&market_index)?; - - let time_since_last_add_liquidity = now.safe_sub(user.last_add_perp_lp_shares_ts)?; - - validate!( - time_since_last_add_liquidity >= state.lp_cooldown_time.cast()?, - ErrorCode::TryingToRemoveLiquidityTooFast - )?; - - controller::funding::settle_funding_payment(user, &user_key, &mut market, now)?; - - let position = &mut user.perp_positions[position_index]; - - validate!( - position.lp_shares >= shares_to_burn, - ErrorCode::InsufficientLPTokens - )?; - - let oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; - let (position_delta, pnl) = - burn_lp_shares(position, &mut market, shares_to_burn, oracle_price)?; - - emit!(LPRecord { - ts: now, - action: LPAction::RemoveLiquidity, - user: user_key, - n_shares: shares_to_burn, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - }); - - Ok(()) -} diff --git a/programs/drift/src/controller/lp/tests.rs b/programs/drift/src/controller/lp/tests.rs deleted file mode 100644 index 2ab426bc76..0000000000 --- a/programs/drift/src/controller/lp/tests.rs +++ /dev/null @@ -1,1818 +0,0 @@ -use crate::controller::lp::*; -use crate::controller::pnl::settle_pnl; -use crate::state::perp_market::AMM; -use crate::state::user::PerpPosition; -use crate::PRICE_PRECISION; -use crate::{SettlePnlMode, BASE_PRECISION_I64}; -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::casting::Cast; -use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_U64, LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, QUOTE_PRECISION_I128, QUOTE_SPOT_MARKET_INDEX, 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, - calculate_perp_position_value_and_pnl, meets_maintenance_margin_requirement, - MarginRequirementType, -}; -use crate::math::position::{ - // get_new_position_amounts, - get_position_update_type, - PositionUpdateType, -}; -use crate::state::margin_calculation::{MarginCalculation, MarginContext}; -use crate::state::oracle::{HistoricalOracleData, OracleSource}; -use crate::state::oracle::{OraclePriceData, StrictOraclePrice}; -use crate::state::oracle_map::OracleMap; -use crate::state::perp_market::{MarketStatus, PerpMarket, PoolBalance}; -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::{SpotPosition, User}; -use crate::test_utils::*; -use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; -use anchor_lang::prelude::Clock; - -#[test] -fn test_lp_wont_collect_improper_funding() { - let mut position = PerpPosition { - base_asset_amount: 1, - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 1, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_short = -10; - market.amm.cumulative_funding_rate_long = -10; - market.amm.cumulative_funding_rate_long = -10; - - let result = settle_lp_position(&mut position, &mut market); - assert_eq!(result, Err(ErrorCode::InvalidPerpPositionDetected)); -} - -#[test] -fn test_full_long_settle() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 1, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_short = -10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 10); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 0); - // net baa doesnt change - assert_eq!( - og_market.amm.base_asset_amount_with_amm, - market.amm.base_asset_amount_with_amm - ); - - // burn - let lp_shares = position.lp_shares; - burn_lp_shares(&mut position, &mut market, lp_shares, 0).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); -} - -#[test] -fn test_full_short_settle() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - peg_multiplier: 1, - user_lp_shares: 100 * AMM_RESERVE_PRECISION, - order_step_size: 1, - ..AMM::default_test() - }; - - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - mint_lp_shares(&mut position, &mut market, 100 * BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = -10; - market.amm.quote_asset_amount_per_lp = 10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, -10); - assert_eq!(position.last_quote_asset_amount_per_lp, 10); - assert_eq!(position.base_asset_amount, -10 * 100); - assert_eq!(position.quote_asset_amount, 10 * 100); -} - -#[test] -fn test_partial_short_settle() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 3, - ..AMM::default_test() - }; - - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = -10; - market.amm.quote_asset_amount_per_lp = 10; - - market.amm.base_asset_amount_with_unsettled_lp = 9; - market.amm.base_asset_amount_long = 9; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.base_asset_amount, -9); - assert_eq!(position.quote_asset_amount, 10); - assert_eq!(position.remainder_base_asset_amount, -1); - assert_eq!(position.last_base_asset_amount_per_lp, -10); - assert_eq!(position.last_quote_asset_amount_per_lp, 10); - - // burn - let _position = position; - let lp_shares = position.lp_shares; - burn_lp_shares(&mut position, &mut market, lp_shares, 0).unwrap(); - assert_eq!(position.lp_shares, 0); -} - -#[test] -fn test_partial_long_settle() { - let mut position = PerpPosition { - lp_shares: BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: -10, - quote_asset_amount_per_lp: 10, - order_step_size: 3, - ..AMM::default_test() - }; - - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.base_asset_amount, -9); - assert_eq!(position.quote_asset_amount, 10); - assert_eq!(position.remainder_base_asset_amount, -1); - assert_eq!(position.last_base_asset_amount_per_lp, -10); - assert_eq!(position.last_quote_asset_amount_per_lp, 10); -} - -#[test] -fn test_remainder_long_settle_too_large_order_step_size() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 5 * BASE_PRECISION_U64, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm = 10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(position.remainder_base_asset_amount, 10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -10); - // net baa doesnt change after settle_lp_position - assert_eq!(market.amm.base_asset_amount_with_amm, 10); - - // burn - let lp_shares = position.lp_shares; - assert_eq!(lp_shares, BASE_PRECISION_U64); - burn_lp_shares(&mut position, &mut market, lp_shares, 22).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); - assert_eq!(position.quote_asset_amount, -11); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 0); -} - -#[test] -fn test_remainder_overflows_too_large_order_step_size() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 5 * BASE_PRECISION_U64, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm = 10; - market.amm.base_asset_amount_short = 0; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(position.remainder_base_asset_amount, 10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -10); - // net baa doesnt change after settle_lp_position - assert_eq!(market.amm.base_asset_amount_with_amm, 10); - - market.amm.base_asset_amount_per_lp += BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp += -16900000000; - market.amm.base_asset_amount_with_unsettled_lp += -(BASE_PRECISION_I128 + 1); - // market.amm.base_asset_amount_short ; - market.amm.base_asset_amount_with_amm += BASE_PRECISION_I128 + 1; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 1000000011); - assert_eq!(position.last_quote_asset_amount_per_lp, -16900000010); - assert_eq!(position.quote_asset_amount, -16900000010); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 1000000011); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // might break i32 limit - market.amm.base_asset_amount_per_lp = 3 * BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp = -(3 * 16900000000); - market.amm.base_asset_amount_with_unsettled_lp = -(3 * BASE_PRECISION_I128 + 1); - market.amm.base_asset_amount_short = -(3 * BASE_PRECISION_I128 + 1); - - // not allowed to settle when remainder is above i32 but below order size - assert!(settle_lp_position(&mut position, &mut market).is_err()); - - // assert_eq!(position.last_base_asset_amount_per_lp, 1000000001); - // assert_eq!(position.last_quote_asset_amount_per_lp, -16900000000); - assert_eq!(position.quote_asset_amount, -16900000010); - assert_eq!(position.base_asset_amount, 0); - // assert_eq!(position.remainder_base_asset_amount, 1000000001); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // past order_step_size on market - market.amm.base_asset_amount_per_lp = 5 * BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp = -116900000000; - market.amm.base_asset_amount_with_unsettled_lp = -(5 * BASE_PRECISION_I128 + 1); - market.amm.base_asset_amount_short = -(5 * BASE_PRECISION_I128); - market.amm.base_asset_amount_with_amm = 1; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -1); - assert_eq!(market.amm.base_asset_amount_short, -5000000000); - assert_eq!(market.amm.base_asset_amount_long, 5 * BASE_PRECISION_I128); - assert_eq!(market.amm.base_asset_amount_with_amm, 1); - - assert_eq!(position.last_base_asset_amount_per_lp, 5000000001); - assert_eq!(position.last_quote_asset_amount_per_lp, -116900000000); - assert_eq!(position.quote_asset_amount, -116900000000); - assert_eq!(position.base_asset_amount, 5000000000); - assert_eq!(position.remainder_base_asset_amount, 1); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // burn - let lp_shares = position.lp_shares; - assert_eq!(lp_shares, BASE_PRECISION_U64); - burn_lp_shares(&mut position, &mut market, lp_shares, 22).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); - assert_eq!(position.quote_asset_amount, -116900000001); - assert_eq!(position.base_asset_amount, 5000000000); - assert_eq!(position.remainder_base_asset_amount, 0); - - assert_eq!(market.amm.base_asset_amount_with_amm, 0); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 0); - assert_eq!(market.amm.base_asset_amount_short, -5000000000); - assert_eq!(market.amm.base_asset_amount_long, 5000000000); -} - -#[test] -fn test_remainder_burn_large_order_step_size() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 2 * BASE_PRECISION_U64, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm += 10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(position.remainder_base_asset_amount, 10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -10); - // net baa doesnt change after settle_lp_position - assert_eq!(market.amm.base_asset_amount_with_amm, 10); - - market.amm.base_asset_amount_per_lp = BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp = -16900000000; - market.amm.base_asset_amount_with_unsettled_lp += -(BASE_PRECISION_I128 + 1); - market.amm.base_asset_amount_with_amm += BASE_PRECISION_I128 + 1; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 1000000001); - assert_eq!(position.last_quote_asset_amount_per_lp, -16900000000); - assert_eq!(position.quote_asset_amount, -16900000000); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 1000000001); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // burn with overflowed remainder - let lp_shares = position.lp_shares; - assert_eq!(lp_shares, BASE_PRECISION_U64); - burn_lp_shares(&mut position, &mut market, lp_shares, 22).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); - assert_eq!(position.quote_asset_amount, -16900000023); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 0); -} - -#[test] -pub fn test_lp_settle_pnl() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - position.last_cumulative_funding_rate = 1337; - - 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 clock = Clock { - slot: 0, - epoch_start_timestamp: 0, - epoch: 0, - leader_schedule_epoch: 0, - unix_timestamp: 0, - }; - 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: 2 * BASE_PRECISION_U64 / 100, - 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, - concentration_coef: 1000001, - 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() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm += 10; - market.amm.cumulative_funding_rate_long = 169; - market.amm.cumulative_funding_rate_short = 169; - - settle_lp_position(&mut position, &mut market).unwrap(); - 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, - deposit_balance: 100 * SPOT_BALANCE_PRECISION, - ..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 user_key = Pubkey::default(); - let authority = Pubkey::default(); - - let mut user = User { - perp_positions: get_positions(position), - 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 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 MarginCalculation { - total_collateral: total_collateral1, - margin_requirement: margin_requirement1, - .. - } = calculate_margin_requirement_and_total_collateral_and_liability_info( - &user, - &market_map, - &spot_market_map, - &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial), - ) - .unwrap(); - - assert_eq!(total_collateral1, 49999988); - assert_eq!(margin_requirement1, 2099020); // $2+ for margin req - - let result = settle_pnl( - 0, - &mut user, - &authority, - &user_key, - &market_map, - &spot_market_map, - &mut oracle_map, - &clock, - &state, - None, - SettlePnlMode::MustSettle, - ); - - assert_eq!(result, Ok(())); - // assert_eq!(result, Err(ErrorCode::InsufficientCollateralForSettlingPNL)) -} - -#[test] -fn test_lp_margin_calc() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - position.last_cumulative_funding_rate = 1337; - - 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 slot = 0; - 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: 2 * BASE_PRECISION_U64 / 100, - 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, - concentration_coef: 1000001, - 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() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 100 * BASE_PRECISION_I128; - market.amm.quote_asset_amount_per_lp = -BASE_PRECISION_I128; - market.amm.base_asset_amount_with_unsettled_lp = -100 * BASE_PRECISION_I128; - market.amm.base_asset_amount_short = -100 * BASE_PRECISION_I128; - market.amm.cumulative_funding_rate_long = 169 * 100000000; - market.amm.cumulative_funding_rate_short = 169 * 100000000; - - settle_lp_position(&mut position, &mut market).unwrap(); - 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, - deposit_balance: 100 * SPOT_BALANCE_PRECISION, - ..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(position), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 5000 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - user.perp_positions[0].base_asset_amount = BASE_PRECISION_I128 as i64; - - // user has lp shares + long and last cumulative funding doesnt match - assert_eq!(user.perp_positions[0].lp_shares, 1000000000); - assert_eq!( - user.perp_positions[0].base_asset_amount, - BASE_PRECISION_I128 as i64 - ); - assert!( - user.perp_positions[0].last_cumulative_funding_rate != market.amm.last_funding_rate_long - ); - - let result = - meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map); - - assert_eq!(result.unwrap(), true); - - // add move lower - let oracle_price_data = OraclePriceData { - price: oracle_price.agg.price, - confidence: 100000, - delay: 1, - has_sufficient_number_of_data_points: true, - }; - - assert_eq!(market.amm.base_asset_amount_per_lp, 100000000000); - assert_eq!(market.amm.quote_asset_amount_per_lp, -1000000000); - assert_eq!(market.amm.cumulative_funding_rate_long, 16900000000); - assert_eq!(market.amm.cumulative_funding_rate_short, 16900000000); - - assert_eq!(user.perp_positions[0].lp_shares, 1000000000); - assert_eq!(user.perp_positions[0].base_asset_amount, 1000000000); - assert_eq!( - user.perp_positions[0].last_base_asset_amount_per_lp, - 100000000000 - ); - assert_eq!( - user.perp_positions[0].last_quote_asset_amount_per_lp, - -1000000000 - ); - assert_eq!( - user.perp_positions[0].last_cumulative_funding_rate, - 16900000000 - ); - - // increase markets so user has to settle lp - market.amm.base_asset_amount_per_lp *= 2; - market.amm.quote_asset_amount_per_lp *= 20; - - // update funding so user has unsettled funding - market.amm.cumulative_funding_rate_long *= 2; - market.amm.cumulative_funding_rate_short *= 2; - - apply_lp_rebase_to_perp_market(&mut market, 1).unwrap(); - - let sim_user_pos = user.perp_positions[0] - .simulate_settled_lp_position(&market, oracle_price_data.price) - .unwrap(); - assert_ne!( - sim_user_pos.base_asset_amount, - user.perp_positions[0].base_asset_amount - ); - assert_eq!(sim_user_pos.base_asset_amount, 101000000000); - assert_eq!(sim_user_pos.quote_asset_amount, -20000000000); - assert_eq!(sim_user_pos.last_cumulative_funding_rate, 16900000000); - - let strict_quote_price = StrictOraclePrice::test(1000000); - // ensure margin calc doesnt incorrectly count funding rate (funding pnl MUST come before settling lp) - let ( - margin_requirement, - weighted_unrealized_pnl, - worse_case_base_asset_value, - _open_order_fraction, - _base_asset_value, - ) = calculate_perp_position_value_and_pnl( - &user.perp_positions[0], - &market, - &oracle_price_data, - &strict_quote_price, - crate::math::margin::MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - assert_eq!(margin_requirement, 1012000000); // $1010 + $2 mr for lp_shares - assert_eq!(weighted_unrealized_pnl, -9916900000); // $-9900000000 upnl (+ -16900000 from old funding) - assert_eq!(worse_case_base_asset_value, 10100000000); //$10100 -} - -#[test] -fn test_lp_has_correct_entry_be_price() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 100000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 101000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - - market.amm.base_asset_amount_per_lp = BASE_PRECISION_I128; - market.amm.quote_asset_amount_per_lp = -99_999_821; - market.amm.base_asset_amount_with_unsettled_lp = BASE_PRECISION_I128; - market.amm.base_asset_amount_long = BASE_PRECISION_I128; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(position.get_entry_price().unwrap(), 99999821); - - assert_eq!(position.quote_entry_amount, -99999821); - assert_eq!(position.quote_break_even_amount, -99999821); - assert_eq!(position.quote_asset_amount, -99999821); - - market.amm.base_asset_amount_per_lp -= BASE_PRECISION_I128 / 2; - market.amm.quote_asset_amount_per_lp += 97_999_821; - market.amm.base_asset_amount_with_unsettled_lp = BASE_PRECISION_I128 / 2; - market.amm.base_asset_amount_long = BASE_PRECISION_I128 / 2; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(position.get_entry_price().unwrap(), 99999822); - - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.quote_entry_amount, -49999911); - assert_eq!(position.quote_break_even_amount, -49999911); - assert_eq!(position.quote_asset_amount, -2000000); - assert_eq!(position.base_asset_amount, 500_000_000); - - let base_delta = -BASE_PRECISION_I128 / 4; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp += 98_999_821 / 4; - let (update_base_delta, _) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - base_delta, - market.amm.order_step_size as u128, - ) - .unwrap(); - - market.amm.base_asset_amount_with_unsettled_lp += update_base_delta; - market.amm.base_asset_amount_long += update_base_delta; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(position.get_entry_price().unwrap(), 99999824); - assert_eq!(position.get_cost_basis().unwrap(), -75833183); - - assert_eq!(position.base_asset_amount, 300000000); - assert_eq!(position.remainder_base_asset_amount, -50000000); - assert_eq!(position.quote_entry_amount, -24999956); - assert_eq!(position.quote_break_even_amount, -24999956); - assert_eq!(position.quote_asset_amount, 22749955); -} - -#[test] -fn test_lp_has_correct_entry_be_price_sim_no_remainders() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 2000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 0); - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.base_asset_amount, 0); - let mut num_position_flips = 0; - let mut flip_indexes: Vec = Vec::new(); - - for i in 0..3000 { - if i % 3 == 0 { - let px = 100_000_000 - i; - let multi = i % 19 + 1; - let divisor = 10; - let base_delta = -BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp += px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += base_delta; - market.amm.base_asset_amount_short += base_delta; - } else { - // buy - let px = 99_199_821 + i; - let multi = i % 5 + 1; - let divisor = 5; - let base_delta = BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp -= px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += base_delta; - market.amm.base_asset_amount_long += base_delta; - } - - let position_base_before = position.base_asset_amount; - - settle_lp_position(&mut position, &mut market).unwrap(); - - if position_base_before.signum() != position.base_asset_amount.signum() { - num_position_flips += 1; - flip_indexes.push(i); - } - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - let iii = position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount as i64) - .unwrap(); - msg!( - "{}: entry: {}, be: {} cb:{} ({}/{})", - i, - entry, - be, - cb, - iii, - position.base_asset_amount, - ); - assert_eq!(position.remainder_base_asset_amount, 0); - - if position.get_base_asset_amount_with_remainder_abs().unwrap() != 0 { - assert!(entry <= 100 * PRICE_PRECISION as i128); - assert!(entry >= 99 * PRICE_PRECISION as i128); - } - } - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - assert_eq!(position.base_asset_amount, 200500000000); - assert_eq!(entry, 99202392); - assert_eq!(be, 99202392); - assert_eq!(cb, 95227357); - assert_eq!(num_position_flips, 4); - assert_eq!(flip_indexes, [0, 1, 18, 19]); -} - -#[test] -fn test_lp_remainder_position_updates() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(880), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-881), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, -1); - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: -199 * 1000000, - base_asset_amount: BASE_PRECISION_I64, - remainder_base_asset_amount: Some(-BASE_PRECISION_I64 / 22), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - assert_eq!(position.base_asset_amount, 1000000000); - assert_eq!(position.remainder_base_asset_amount, -45454546); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: 199 * 1000000, - base_asset_amount: -BASE_PRECISION_I64 * 2, - remainder_base_asset_amount: Some(BASE_PRECISION_I64 / 23), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, -101912122); - assert_eq!(position.base_asset_amount, -1000000000); - assert_eq!(position.remainder_base_asset_amount, -1976286); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); -} - -#[test] -fn test_lp_remainder_position_updates_2() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 300000000, - remainder_base_asset_amount: Some(33333333), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 500000000, - remainder_base_asset_amount: Some(0), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - assert_eq!(position.base_asset_amount, 800000000); - assert_eq!(position.remainder_base_asset_amount, 33333333); - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: 199 * 10000, - base_asset_amount: -300000000, - remainder_base_asset_amount: Some(-63636363), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 1990000); - assert_eq!(position.base_asset_amount, 500000000); - assert_eq!(position.remainder_base_asset_amount, -30303030); - assert_eq!(market.amm.base_asset_amount_long, 500000000); - assert_eq!(market.amm.base_asset_amount_short, 0); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 500000000); - assert_eq!(market.amm.base_asset_amount_with_amm, 0); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); -} - -#[test] -fn test_lp_has_correct_entry_be_price_sim() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 2000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 0); - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.base_asset_amount, 0); - let mut num_position_flips = 0; - let mut flip_indexes: Vec = Vec::new(); - - let mut total_remainder = 0; - for i in 0..3000 { - if i % 3 == 0 { - let px = 100_000_000 - i; - let multi = i % 19 + 1; - let divisor = 11; - let base_delta = -BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp += px * multi / divisor; - - let (update_base_delta, rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - base_delta, - market.amm.order_step_size as u128, - ) - .unwrap(); - total_remainder += rr; - - let (total_remainder_f, _rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - total_remainder, - market.amm.order_step_size as u128, - ) - .unwrap(); - if total_remainder_f != 0 { - total_remainder -= total_remainder_f; - msg!("total_remainder update {}", total_remainder); - } - - market.amm.base_asset_amount_with_unsettled_lp += update_base_delta; - market.amm.base_asset_amount_long += update_base_delta; - } else { - // buy - let px = 99_199_821 + i; - let multi = i % 5 + 1; - let divisor = 6; - let base_delta = BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp -= px * multi / divisor; - - let (update_base_delta, rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - base_delta, - market.amm.order_step_size as u128, - ) - .unwrap(); - total_remainder += rr; - - let (total_remainder_f, _rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - total_remainder, - market.amm.order_step_size as u128, - ) - .unwrap(); - if total_remainder_f != 0 { - total_remainder -= total_remainder_f; - } - - market.amm.base_asset_amount_with_unsettled_lp += update_base_delta; - market.amm.base_asset_amount_short += update_base_delta; - } - - let position_base_before = position.base_asset_amount; - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - settle_lp_position(&mut position, &mut market).unwrap(); - - if position_base_before.signum() != position.base_asset_amount.signum() { - num_position_flips += 1; - flip_indexes.push(i); - } - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - let iii = position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount as i64) - .unwrap(); - msg!( - "{}: entry: {}, be: {} cb:{} ({}/{})", - i, - entry, - be, - cb, - iii, - position.base_asset_amount, - ); - // assert_ne!(position.remainder_base_asset_amount, 0); - - if position.get_base_asset_amount_with_remainder_abs().unwrap() != 0 { - assert!(entry <= 100 * PRICE_PRECISION as i128); - assert!(entry >= 99 * PRICE_PRECISION as i128); - } - } - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - assert_eq!(entry, 99202570); - assert_eq!(be, 99202570); - assert_eq!(cb, 91336780); - assert_eq!(num_position_flips, 5); - assert_eq!(flip_indexes, [1, 18, 19, 36, 37]); - assert_eq!(position.base_asset_amount, 91300000000); -} - -#[test] -fn test_lp_has_correct_entry_be_price_sim_more_flips() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 2000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 0); - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.base_asset_amount, 0); - let mut num_position_flips = 0; - let mut flip_indexes: Vec = Vec::new(); - - for i in 0..3000 { - if i % 2 == 0 { - let px = 99_800_000 - i * i % 4; - let multi = i % 7 + 1 + i; - let divisor = 10; - let amt2 = -BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += amt2; - market.amm.quote_asset_amount_per_lp += px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += amt2; - market.amm.base_asset_amount_short += amt2; - } else { - // buy - let px = 99_199_821 + i * i % 4; - let multi = i % 7 + 1 + i; - let divisor = 10; - let base_delta = BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp -= px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += base_delta; - market.amm.base_asset_amount_long += base_delta; - } - - let position_base_before = position.base_asset_amount; - - settle_lp_position(&mut position, &mut market).unwrap(); - - if position_base_before.signum() != position.base_asset_amount.signum() { - num_position_flips += 1; - flip_indexes.push(i); - } - assert_eq!(position.remainder_base_asset_amount, 0); - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - let iii = position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount as i64) - .unwrap(); - msg!( - "{}: entry: {}, be: {} cb:{} ({}/{})", - i, - entry, - be, - cb, - iii, - position.base_asset_amount, - ); - - if position.get_base_asset_amount_with_remainder_abs().unwrap() != 0 { - assert!(entry <= 99_800_000_i128); - assert!(entry >= 99_199_820_i128); - } - } - - assert_eq!(num_position_flips, 3000); - // assert_eq!(flip_indexes, [0, 1, 18, 19]); - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - assert_eq!(position.base_asset_amount, 150200000000); - assert_eq!(position.remainder_base_asset_amount, 0); - - assert_eq!(entry, 99199822); - assert_eq!(be, 99199822); - assert_eq!(cb, -801664962); -} - -#[test] -fn test_get_position_update_type_lp_opens() { - // position is empty, every inc must be open - let position = PerpPosition { - ..PerpPosition::default() - }; - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let position = PerpPosition { - ..PerpPosition::default() - }; - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); -} - -#[test] -fn test_get_position_update_type_lp_negative_position() { - // $119 short - let position = PerpPosition { - base_asset_amount: -1000000000 * 2, - quote_asset_amount: 119000000 * 2, - quote_entry_amount: 119000000 * 2, - quote_break_even_amount: 119000000 * 2, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 119000000); - assert_eq!(position.get_breakeven_price().unwrap(), 119000000); - assert_eq!(position.get_entry_price().unwrap(), 119000000); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); // more negative - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81, - remainder_base_asset_amount: Some(-81000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81000, - remainder_base_asset_amount: Some(-81), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); -} - -#[test] -fn test_get_position_update_type_lp_positive_position() { - // $119 long - let position = PerpPosition { - base_asset_amount: 1000000000 * 2, - quote_asset_amount: -119000000 * 2, - quote_entry_amount: -119000000 * 2, - quote_break_even_amount: -119000000 * 2, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 119000000); - assert_eq!(position.get_breakeven_price().unwrap(), 119000000); - assert_eq!(position.get_entry_price().unwrap(), 119000000); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // no base/remainder is reduce (should be skipped earlier) - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81, - remainder_base_asset_amount: Some(-81000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81000, - remainder_base_asset_amount: Some(-81), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); -} - -#[test] -fn test_get_position_update_type_lp_positive_position_with_positive_remainder() { - // $119 long - let position = PerpPosition { - base_asset_amount: 1000000000 * 2, - remainder_base_asset_amount: 7809809, - quote_asset_amount: -119000000 * 2, - quote_entry_amount: -119000000 * 2, - quote_break_even_amount: -119000000 * 2, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 119000000); - assert_eq!(position.get_breakeven_price().unwrap(), 118537123); - assert_eq!(position.get_entry_price().unwrap(), 118537123); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000001 * 2, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-7809809), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: Some(-7809809 - 1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // no base/remainder is reduce (should be skipped earlier) - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81, - remainder_base_asset_amount: Some(-81000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81000, - remainder_base_asset_amount: Some(-81), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); -} - -#[test] -fn test_get_position_update_type_positive_remainder() { - // $119 long (only a remainder size) - let position = PerpPosition { - base_asset_amount: 0, - remainder_base_asset_amount: 7809809, - quote_asset_amount: -119000000 * 7809809 / BASE_PRECISION_I64, - quote_entry_amount: -119000000 * 7809809 / BASE_PRECISION_I64, - quote_break_even_amount: -119000000 * 7809809 / BASE_PRECISION_I64, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 118999965); - assert_eq!(position.get_entry_price().unwrap(), 118999965); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000001 * 2, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-8791), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-7809809), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Close - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-7809809 - 1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: Some(-7809809 - 1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: None, - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller -} diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index ecd9a3a6ab..1565eb1174 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -2,7 +2,6 @@ pub mod amm; pub mod funding; pub mod insurance; pub mod liquidation; -pub mod lp; pub mod orders; pub mod pda; pub mod pnl; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 5d9818172b..8884c1695e 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -8,11 +8,10 @@ use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use anchor_lang::prelude::*; use crate::controller::funding::settle_funding_payment; -use crate::controller::lp::burn_lp_shares; use crate::controller::position; use crate::controller::position::{ add_new_position, decrease_open_bids_and_asks, get_position_index, increase_open_bids_and_asks, - update_lp_market_position, update_position_and_market, update_quote_asset_amount, + update_position_and_market, update_quote_asset_amount, PositionDirection, }; use crate::controller::spot_balance::{ @@ -37,7 +36,6 @@ use crate::math::fulfillment::{ determine_perp_fulfillment_methods, determine_spot_fulfillment_methods, }; use crate::math::liquidation::validate_user_not_being_liquidated; -use crate::math::lp::calculate_lp_shares_to_burn_for_risk_reduction; use crate::math::matching::{ are_orders_same_market_but_different_sides, calculate_fill_for_matched_orders, calculate_filler_multiplier_for_matched_orders, do_orders_cross, is_maker_for_taker, @@ -995,7 +993,7 @@ pub fn fill_perp_order( // settle lp position so its tradeable let mut market = perp_market_map.get_ref_mut(&market_index)?; - controller::lp::settle_funding_payment_then_lp(user, &user_key, &mut market, now)?; + settle_funding_payment(user, &user_key, &mut market, now)?; validate!( matches!( @@ -2238,29 +2236,6 @@ pub fn fulfill_perp_order_with_amm( let user_position_delta = get_position_delta_for_fill(base_asset_amount, quote_asset_amount, order_direction)?; - if liquidity_split != AMMLiquiditySplit::ProtocolOwned { - update_lp_market_position( - market, - &user_position_delta, - fee_to_market_for_lp.cast()?, - liquidity_split, - )?; - } - - if market.amm.user_lp_shares > 0 { - let (new_terminal_quote_reserve, new_terminal_base_reserve) = - crate::math::amm::calculate_terminal_reserves(&market.amm)?; - market.amm.terminal_quote_asset_reserve = new_terminal_quote_reserve; - - let (min_base_asset_reserve, max_base_asset_reserve) = - crate::math::amm::calculate_bid_ask_bounds( - market.amm.concentration_coef, - new_terminal_base_reserve, - )?; - market.amm.min_base_asset_reserve = min_base_asset_reserve; - market.amm.max_base_asset_reserve = max_base_asset_reserve; - } - // Increment the protocol's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; market.amm.total_exchange_fee = market.amm.total_exchange_fee.safe_add(user_fee.cast()?)?; @@ -3293,129 +3268,6 @@ pub fn can_reward_user_with_perp_pnl(user: &mut Option<&mut User>, market_index: } } -pub fn attempt_burn_user_lp_shares_for_risk_reduction( - state: &State, - user: &mut User, - user_key: Pubkey, - margin_calc: MarginCalculation, - perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, - oracle_map: &mut OracleMap, - clock: &Clock, - market_index: u16, -) -> DriftResult { - let now = clock.unix_timestamp; - let time_since_last_liquidity_change: i64 = now.safe_sub(user.last_add_perp_lp_shares_ts)?; - // avoid spamming update if orders have already been set - if time_since_last_liquidity_change >= state.lp_cooldown_time.cast()? { - burn_user_lp_shares_for_risk_reduction( - state, - user, - user_key, - market_index, - margin_calc, - perp_market_map, - spot_market_map, - oracle_map, - clock, - )?; - user.last_add_perp_lp_shares_ts = now; - } - - Ok(()) -} - -pub fn burn_user_lp_shares_for_risk_reduction( - state: &State, - user: &mut User, - user_key: Pubkey, - market_index: u16, - margin_calc: MarginCalculation, - perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, - oracle_map: &mut OracleMap, - clock: &Clock, -) -> DriftResult { - let position_index = get_position_index(&user.perp_positions, market_index)?; - let is_lp = user.perp_positions[position_index].is_lp(); - if !is_lp { - return Ok(()); - } - - let mut market = perp_market_map.get_ref_mut(&market_index)?; - - let quote_oracle_id = spot_market_map - .get_ref(&market.quote_spot_market_index)? - .oracle_id(); - let quote_oracle_price = oracle_map.get_price_data("e_oracle_id)?.price; - - let oracle_price_data = oracle_map.get_price_data(&market.oracle_id())?; - - let oracle_price = if market.status == MarketStatus::Settlement { - market.expiry_price - } else { - oracle_price_data.price - }; - - let user_custom_margin_ratio = user.max_margin_ratio; - let (lp_shares_to_burn, base_asset_amount_to_close) = - calculate_lp_shares_to_burn_for_risk_reduction( - &user.perp_positions[position_index], - &market, - oracle_price, - quote_oracle_price, - margin_calc.margin_shortage()?, - user_custom_margin_ratio, - user.is_high_leverage_mode(), - )?; - - let (position_delta, pnl) = burn_lp_shares( - &mut user.perp_positions[position_index], - &mut market, - lp_shares_to_burn, - oracle_price, - )?; - - // emit LP record for shares removed - emit_stack::<_, { LPRecord::SIZE }>(LPRecord { - ts: clock.unix_timestamp, - action: LPAction::RemoveLiquidityDerisk, - user: user_key, - n_shares: lp_shares_to_burn, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - })?; - - let direction_to_close = user.perp_positions[position_index].get_direction_to_close(); - - let params = OrderParams::get_close_perp_params( - &market, - direction_to_close, - base_asset_amount_to_close, - )?; - - drop(market); - - if user.has_room_for_new_order() { - controller::orders::place_perp_order( - state, - user, - user_key, - perp_market_map, - spot_market_map, - oracle_map, - &None, - clock, - params, - PlaceOrderOptions::default().explanation(OrderActionExplanation::DeriskLp), - )?; - } - - Ok(()) -} - pub fn pay_keeper_flat_reward_for_perps( user: &mut User, filler: Option<&mut User>, diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 13c29ada7d..46cf1e9779 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -1,7 +1,7 @@ use crate::controller::amm::{update_pnl_pool_and_user_balance, update_pool_balances}; use crate::controller::funding::settle_funding_payment; use crate::controller::orders::{ - attempt_burn_user_lp_shares_for_risk_reduction, cancel_orders, + cancel_orders, validate_market_within_price_band, }; use crate::controller::position::{ @@ -74,7 +74,7 @@ pub fn settle_pnl( validate_market_within_price_band(&market, state, oracle_price)?; - crate::controller::lp::settle_funding_payment_then_lp(user, user_key, &mut market, now)?; + settle_funding_payment(user, user_key, &mut market, now)?; drop(market); @@ -82,48 +82,7 @@ pub fn settle_pnl( let unrealized_pnl = user.perp_positions[position_index].get_unrealized_pnl(oracle_price)?; // cannot settle negative pnl this way on a user who is in liquidation territory - if user.perp_positions[position_index].is_lp() && !user.is_advanced_lp() { - let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( - user, - perp_market_map, - spot_market_map, - oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(state.liquidation_margin_buffer_ratio), - )?; - - if !margin_calc.meets_margin_requirement() { - msg!("market={} lp does not meet initial margin requirement, attempting to burn shares for risk reduction", - market_index); - attempt_burn_user_lp_shares_for_risk_reduction( - state, - user, - *user_key, - margin_calc, - perp_market_map, - spot_market_map, - oracle_map, - clock, - market_index, - )?; - - // if the unrealized pnl is negative, return early after trying to burn shares - if unrealized_pnl < 0 - && !(meets_settle_pnl_maintenance_margin_requirement( - user, - perp_market_map, - spot_market_map, - oracle_map, - )?) - { - msg!( - "Unable to settle market={} negative pnl as user is in liquidation territory", - market_index - ); - return Ok(()); - } - } - } else if unrealized_pnl < 0 { + if unrealized_pnl < 0 { // may already be cached let meets_margin_requirement = match meets_margin_requirement { Some(meets_margin_requirement) => meets_margin_requirement, @@ -436,12 +395,6 @@ pub fn settle_expired_position( "User must first cancel open orders for expired market" )?; - validate!( - user.perp_positions[position_index].lp_shares == 0, - ErrorCode::PerpMarketSettlementUserHasActiveLP, - "User must first burn lp shares for expired market" - )?; - let base_asset_value = calculate_base_asset_value_with_expiry_price( &user.perp_positions[position_index], perp_market.expiry_price, @@ -453,7 +406,6 @@ pub fn settle_expired_position( let position_delta = PositionDelta { quote_asset_amount: base_asset_value, base_asset_amount: -user.perp_positions[position_index].base_asset_amount, - remainder_base_asset_amount: None, }; update_position_and_market( diff --git a/programs/drift/src/controller/position.rs b/programs/drift/src/controller/position.rs index a84e4c951a..fea9ef135a 100644 --- a/programs/drift/src/controller/position.rs +++ b/programs/drift/src/controller/position.rs @@ -74,21 +74,11 @@ pub fn get_position_index(user_positions: &PerpPositions, market_index: u16) -> pub struct PositionDelta { pub quote_asset_amount: i64, pub base_asset_amount: i64, - pub remainder_base_asset_amount: Option, } impl PositionDelta { - pub fn get_delta_base_with_remainder_abs(&self) -> DriftResult { - let delta_base_i128 = - if let Some(remainder_base_asset_amount) = self.remainder_base_asset_amount { - self.base_asset_amount - .safe_add(remainder_base_asset_amount.cast()?)? - .abs() - .cast::()? - } else { - self.base_asset_amount.abs().cast::()? - }; - Ok(delta_base_i128) + pub fn get_delta_base_abs(&self) -> DriftResult { + self.base_asset_amount.abs().cast::() } } @@ -97,7 +87,7 @@ pub fn update_position_and_market( market: &mut PerpMarket, delta: &PositionDelta, ) -> DriftResult { - if delta.base_asset_amount == 0 && delta.remainder_base_asset_amount.unwrap_or(0) == 0 { + if delta.base_asset_amount == 0 { update_quote_asset_amount(position, market, delta.quote_asset_amount)?; return Ok(delta.quote_asset_amount); } @@ -107,12 +97,8 @@ pub fn update_position_and_market( // Update User let ( new_base_asset_amount, - new_settled_base_asset_amount, new_quote_asset_amount, - new_remainder_base_asset_amount, - ) = get_new_position_amounts(position, delta, market)?; - - market.update_market_with_counterparty(delta, new_settled_base_asset_amount)?; + ) = get_new_position_amounts(position, delta)?; let (new_quote_entry_amount, new_quote_break_even_amount, pnl) = match update_type { PositionUpdateType::Open | PositionUpdateType::Increase => { @@ -127,8 +113,8 @@ pub fn update_position_and_market( (new_quote_entry_amount, new_quote_break_even_amount, 0_i64) } PositionUpdateType::Reduce | PositionUpdateType::Close => { - let current_base_i128 = position.get_base_asset_amount_with_remainder_abs()?; - let delta_base_i128 = delta.get_delta_base_with_remainder_abs()?; + let current_base_i128 = position.get_base_asset_amount_abs()?; + let delta_base_i128 = delta.get_delta_base_abs()?; let new_quote_entry_amount = position.quote_entry_amount.safe_sub( position @@ -156,8 +142,8 @@ pub fn update_position_and_market( (new_quote_entry_amount, new_quote_break_even_amount, pnl) } PositionUpdateType::Flip => { - let current_base_i128 = position.get_base_asset_amount_with_remainder_abs()?; - let delta_base_i128 = delta.get_delta_base_with_remainder_abs()?; + let current_base_i128 = position.get_base_asset_amount_abs()?; + let delta_base_i128 = delta.get_delta_base_abs()?; // same calculation for new_quote_entry_amount let new_quote_break_even_amount = delta.quote_asset_amount.safe_sub( @@ -209,7 +195,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_long = market .amm .base_asset_amount_long - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_long = market .amm .quote_entry_amount_long @@ -223,7 +209,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_short = market .amm .base_asset_amount_short - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_short = market .amm .quote_entry_amount_short @@ -239,7 +225,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_long = market .amm .base_asset_amount_long - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_long = market.amm.quote_entry_amount_long.safe_sub( position .quote_entry_amount @@ -257,7 +243,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_short = market .amm .base_asset_amount_short - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_short = market.amm.quote_entry_amount_short.safe_sub( position @@ -358,9 +344,6 @@ pub fn update_position_and_market( _ => {} } - let new_position_base_with_remainder = - new_base_asset_amount.safe_add(new_remainder_base_asset_amount)?; - // Update user position if let PositionUpdateType::Close = update_type { position.last_cumulative_funding_rate = 0; @@ -368,7 +351,7 @@ pub fn update_position_and_market( update_type, PositionUpdateType::Open | PositionUpdateType::Increase | PositionUpdateType::Flip ) { - if new_position_base_with_remainder > 0 { + if new_base_asset_amount > 0 { position.last_cumulative_funding_rate = market.amm.cumulative_funding_rate_long.cast()?; } else { @@ -388,7 +371,6 @@ pub fn update_position_and_market( new_base_asset_amount )?; - position.remainder_base_asset_amount = new_remainder_base_asset_amount.cast::()?; position.base_asset_amount = new_base_asset_amount; position.quote_asset_amount = new_quote_asset_amount; @@ -398,68 +380,6 @@ pub fn update_position_and_market( Ok(pnl) } -pub fn update_lp_market_position( - market: &mut PerpMarket, - delta: &PositionDelta, - fee_to_market: i128, - liquidity_split: AMMLiquiditySplit, -) -> DriftResult { - if market.amm.user_lp_shares == 0 || liquidity_split == AMMLiquiditySplit::ProtocolOwned { - return Ok(0); // no need to split with LP - } - - let base_unit: i128 = market.amm.get_per_lp_base_unit()?; - - let (per_lp_delta_base, per_lp_delta_quote, per_lp_fee) = - market - .amm - .calculate_per_lp_delta(delta, fee_to_market, liquidity_split, base_unit)?; - - market.amm.base_asset_amount_per_lp = market - .amm - .base_asset_amount_per_lp - .safe_add(-per_lp_delta_base)?; - - market.amm.quote_asset_amount_per_lp = market - .amm - .quote_asset_amount_per_lp - .safe_add(-per_lp_delta_quote)?; - - // track total fee earned by lps (to attribute breakdown of IL) - market.amm.total_fee_earned_per_lp = market - .amm - .total_fee_earned_per_lp - .saturating_add(per_lp_fee.cast()?); - - // update per lp position - market.amm.quote_asset_amount_per_lp = - market.amm.quote_asset_amount_per_lp.safe_add(per_lp_fee)?; - - let lp_delta_base = market - .amm - .calculate_lp_base_delta(per_lp_delta_base, base_unit)?; - let lp_delta_quote = market - .amm - .calculate_lp_base_delta(per_lp_delta_quote, base_unit)?; - - market.amm.base_asset_amount_with_amm = market - .amm - .base_asset_amount_with_amm - .safe_sub(lp_delta_base)?; - - market.amm.base_asset_amount_with_unsettled_lp = market - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(lp_delta_base)?; - - market.amm.quote_asset_amount_with_unsettled_lp = market - .amm - .quote_asset_amount_with_unsettled_lp - .safe_add(lp_delta_quote.cast()?)?; - - Ok(lp_delta_base) -} - pub fn update_position_with_base_asset_amount( base_asset_amount: u64, direction: PositionDirection, @@ -557,7 +477,6 @@ pub fn update_quote_asset_amount( if position.quote_asset_amount == 0 && position.base_asset_amount == 0 - && position.remainder_base_asset_amount == 0 { market.number_of_users = market.number_of_users.safe_add(1)?; } @@ -568,7 +487,6 @@ pub fn update_quote_asset_amount( if position.quote_asset_amount == 0 && position.base_asset_amount == 0 - && position.remainder_base_asset_amount == 0 { market.number_of_users = market.number_of_users.saturating_sub(1); } diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 7145c7dabd..bfe693fe44 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -1,9 +1,8 @@ use crate::controller::amm::{ calculate_base_swap_output_with_spread, move_price, recenter_perp_market_amm, swap_base_asset, }; -use crate::controller::lp::{apply_lp_rebase_to_perp_market, settle_lp_position}; use crate::controller::position::{ - update_lp_market_position, update_position_and_market, PositionDelta, + update_position_and_market, PositionDelta, }; use crate::controller::repeg::_update_amm; @@ -13,7 +12,6 @@ use crate::math::constants::{ PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; -use crate::math::lp::calculate_settle_lp_metrics; use crate::math::position::swap_direction_to_close_position; use crate::math::repeg; use crate::state::oracle::{OraclePriceData, PrelaunchOracle}; @@ -43,33 +41,6 @@ use anchor_lang::Owner; use solana_program::pubkey::Pubkey; use std::str::FromStr; -#[test] -fn full_amm_split() { - let delta = PositionDelta { - base_asset_amount: 10 * BASE_PRECISION_I64, - quote_asset_amount: -10 * BASE_PRECISION_I64, - remainder_base_asset_amount: None, - }; - - let amm = AMM { - user_lp_shares: 0, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: 10 * AMM_RESERVE_PRECISION_I128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - update_lp_market_position(&mut market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 0); - assert_eq!( - market.amm.base_asset_amount_with_amm, - 10 * AMM_RESERVE_PRECISION_I128 - ); -} #[test] fn amm_pool_balance_liq_fees_example() { @@ -707,566 +678,12 @@ fn amm_perp_ref_offset() { crate::validation::perp_market::validate_perp_market(&perp_market).unwrap(); } -#[test] -fn amm_split_large_k() { - let perp_market_str = String::from("Ct8MLGv1N/dvAH3EF67yBqaUQerctpm4yqpK+QNSrXCQz76p+B+ka+8Ni2/aLOukHaFdQJXR2jkqDS+O0MbHvA9M+sjCgLVtQwhkAQAAAAAAAAAAAAAAAAIAAAAAAAAAkI1kAQAAAAB6XWQBAAAAAO8yzWQAAAAAnJ7I3f///////////////2dHvwAAAAAAAAAAAAAAAABGiVjX6roAAAAAAAAAAAAAAAAAAAAAAAB1tO47J+xiAAAAAAAAAAAAGD03Fis3mgAAAAAAAAAAAJxiDwAAAAAAAAAAAAAAAABxqRCIGRxiAAAAAAAAAAAAEy8wZfK9YwAAAAAAAAAAAGZeZCE+g3sAAAAAAAAAAAAKYeQAAAAAAAAAAAAAAAAAlIvoyyc3mgAAAAAAAAAAAADQdQKjbgAAAAAAAAAAAAAAwu8g05H/////////////E6tNHAIAAAAAAAAAAAAAAO3mFwd0AAAAAAAAAAAAAAAAgPQg5rUAAAAAAAAAAAAAGkDtXR4AAAAAAAAAAAAAAEv0WeZW/f////////////9kUidaqAIAAAAAAAAAAAAA0ZMEr1H9/////////////w5/U3uqAgAAAAAAAAAAAAAANfbqfCd3AAAAAAAAAAAAIhABAAAAAAAiEAEAAAAAACIQAQAAAAAAY1QBAAAAAAA5f3WMVAAAAAAAAAAAAAAAFhkiihsAAAAAAAAAAAAAAO2EfWc5AAAAAAAAAAAAAACM/5CAQgAAAAAAAAAAAAAAvenX0SsAAAAAAAAAAAAAALgPUogZAAAAAAAAAAAAAAC01x97AAAAAAAAAAAAAAAAOXzVbgAAAAAAAAAAAAAAAMG4+QwBAAAAAAAAAAAAAABwHI3fLeJiAAAAAAAAAAAABvigOblGmgAAAAAAAAAAALeRnZsi9mIAAAAAAAAAAAAqgs3ynCeaAAAAAAAAAAAAQwhkAQAAAAAAAAAAAAAAAJOMZAEAAAAAFKJkAQAAAABTl2QBAAAAALFuZAEAAAAAgrx7DAAAAAAUAwAAAAAAAAN1TAYAAAAAuC7NZAAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAA4fUFAAAAAAAAAAAAAAAAn2HvyMABAADGV6rZFwAAAE5Qg2oPAAAA8zHNZAAAAAAdYAAAAAAAAE2FAAAAAAAA6zLNZAAAAAD6AAAAaEIAABQDAAAUAwAAAAAAANcBAABkADIAZGQAAcDIUt4AAAAA0QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI9qQbynsAAAAAAAAAAAAAAAAAAAAAAAAFNPTC1QRVJQICAgICAgICAgICAgICAgICAgICAgICAghuS1//////8A4fUFAAAAAAB0O6QLAAAAR7PdeQMAAAD+Mc1kAAAAAADKmjsAAAAAAAAAAAAAAAAAAAAAAAAAAOULDwAAAAAAUBkAAAAAAADtAQAAAAAAAMgAAAAAAAAAECcAAKhhAADoAwAA9AEAAAAAAAAQJwAAZAIAAGQCAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let mut perp_market = perp_market_loader.load_mut().unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655); - - let og_baapl = perp_market.amm.base_asset_amount_per_lp; - let og_qaapl = perp_market.amm.quote_asset_amount_per_lp; - - // msg!("perp_market: {:?}", perp_market); - - // min long order for $2.3 - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64 / 10, - quote_asset_amount: -2300000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054758); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655); - - // min short order for $2.3 - let delta = PositionDelta { - base_asset_amount: -BASE_PRECISION_I64 / 10, - quote_asset_amount: 2300000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535654); - - let mut existing_position = PerpPosition { - market_index: 0, - base_asset_amount: 0, - quote_asset_amount: 0, - lp_shares: perp_market.amm.user_lp_shares as u64, - last_base_asset_amount_per_lp: og_baapl as i64, - last_quote_asset_amount_per_lp: og_qaapl as i64, - per_lp_base: 0, - ..PerpPosition::default() - }; - - settle_lp_position(&mut existing_position, &mut perp_market).unwrap(); - - assert_eq!(existing_position.base_asset_amount, 0); - assert_eq!(existing_position.remainder_base_asset_amount, 0); - assert_eq!(existing_position.quote_asset_amount, -33538939); // out of favor rounding - - assert_eq!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - // long order for $230 - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64 * 10, - quote_asset_amount: -230000000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574055043); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535660); - - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_baapl - perp_market.amm.base_asset_amount_per_lp) - / AMM_RESERVE_PRECISION_I128, - 9977763076 - ); - // assert_eq!((perp_market.amm.sqrt_k as i128) * (og_baapl-perp_market.amm.base_asset_amount_per_lp) / AMM_RESERVE_PRECISION_I128, 104297175); - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_qaapl - perp_market.amm.quote_asset_amount_per_lp) - / QUOTE_PRECISION_I128, - -173828625034 - ); - assert_eq!( - (perp_market.amm.sqrt_k as i128) - * (og_qaapl - perp_market.amm.quote_asset_amount_per_lp - 1) - / QUOTE_PRECISION_I128, - -208594350041 - ); - // assert_eq!(243360075047/9977763076 < 23, true); // ensure rounding in favor - - // long order for $230 - let delta = PositionDelta { - base_asset_amount: -BASE_PRECISION_I64 * 10, - quote_asset_amount: 230000000, - remainder_base_asset_amount: None, - }; - - let og_baapl = perp_market.amm.base_asset_amount_per_lp; - let og_qaapl = perp_market.amm.quote_asset_amount_per_lp; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535653); - - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_baapl - perp_market.amm.base_asset_amount_per_lp) - / AMM_RESERVE_PRECISION_I128, - -9977763076 - ); - // assert_eq!((perp_market.amm.sqrt_k as i128) * (og_baapl-perp_market.amm.base_asset_amount_per_lp) / AMM_RESERVE_PRECISION_I128, 104297175); - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_qaapl - perp_market.amm.quote_asset_amount_per_lp) - / QUOTE_PRECISION_I128, - 243360075047 - ); - // assert_eq!(243360075047/9977763076 < 23, true); // ensure rounding in favor -} - -#[test] -fn test_quote_unsettled_lp() { - let perp_market_str = String::from("Ct8MLGv1N/dvAH3EF67yBqaUQerctpm4yqpK+QNSrXCQz76p+B+ka+8Ni2/aLOukHaFdQJXR2jkqDS+O0MbHvA9M+sjCgLVtzjkqCQAAAAAAAAAAAAAAAAIAAAAAAAAAl44wCQAAAAD54C0JAAAAAGJ4JmYAAAAAyqMxdXz//////////////wV1ZyH9//////////////8Uy592jFYPAAAAAAAAAAAAAAAAAAAAAAD6zIP0/dAIAAAAAAAAAAAA+srqThjtHwAAAAAAAAAAAJxiDwAAAAAAAAAAAAAAAAByWgjyVb4IAAAAAAAAAAAAOpuf9pLjCAAAAAAAAAAAAMRfA6LzxhAAAAAAAAAAAABs6IcCAAAAAAAAAAAAAAAAeXyo6oHtHwAAAAAAAAAAAABngilYXAEAAAAAAAAAAAAAZMIneaP+////////////GeN71uL//////////////+fnyHru//////////////8AIA8MEgUDAAAAAAAAAAAAv1P8g/EBAAAAAAAAAAAAACNQgLCty/////////////+KMQ7JGjMAAAAAAAAAAAAA4DK7xH3K/////////////2grSsB0NQAAAAAAAAAAAACsBC7WWDkCAAAAAAAAAAAAsis3AAAAAACyKzcAAAAAALIrNwAAAAAATGc8AAAAAADH51Hn/wYAAAAAAAAAAAAANXNbBAgCAAAAAAAAAAAAAPNHO0UKBQAAAAAAAAAAAABiEweaqQUAAAAAAAAAAAAAg16F138BAAAAAAAAAAAAAFBZFMk0AQAAAAAAAAAAAACoA6JpBwAAAAAAAAAAAAAALahXXQcAAAAAAAAAAAAAAMG4+QwBAAAAAAAAAAAAAADr9qfqkdAIAAAAAAAAAAAAlBk2nZ/uHwAAAAAAAAAAAHPdcUR+0QgAAAAAAAAAAAAF+03DR+sfAAAAAAAAAAAAzjkqCQAAAAAAAAAAAAAAAJXnMAkAAAAAT9IxCQAAAADyXDEJAAAAAKlJLgkAAAAAyg2YDwAAAABfBwAAAAAAANVPrUEAAAAAZW0mZgAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAA4fUFAAAAAAAAAAAAAAAAj0W2KSYpAABzqJhf6gAAAOD5o985AQAAS3gmZgAAAADxKQYAAAAAAMlUBgAAAAAAS3gmZgAAAADuAgAA7CwAAHcBAAC9AQAAAAAAAH0AAADECTIAZMgAAcDIUt4DAAAAFJMfEQAAAADBogAAAAAAAIneROQcpf//AAAAAAAAAAAAAAAAAAAAAFe4ynNxUwoAAAAAAAAAAAAAAAAAAAAAAFNPTC1QRVJQICAgICAgICAgICAgICAgICAgICAgICAgAJsy4v////8AZc0dAAAAAP8PpdToAAAANOVq3RYAAAB7cyZmAAAAAADh9QUAAAAAAAAAAAAAAAAAAAAAAAAAAEyBWwAAAAAA2DEAAAAAAABzBQAAAAAAAMgAAAAAAAAATB0AANQwAADoAwAA9AEAAAAAAAAQJwAAASoAACtgAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let mut perp_market = perp_market_loader.load_mut().unwrap(); - perp_market.amm.quote_asset_amount_with_unsettled_lp = 0; - - let mut existing_position: PerpPosition = PerpPosition::default(); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, -12324473595); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -564969495606); - - existing_position.last_quote_asset_amount_per_lp = - perp_market.amm.quote_asset_amount_per_lp as i64; - existing_position.last_base_asset_amount_per_lp = - perp_market.amm.base_asset_amount_per_lp as i64; - - let pos_delta = PositionDelta { - quote_asset_amount: QUOTE_PRECISION_I64 * 150, - base_asset_amount: -BASE_PRECISION_I64, - remainder_base_asset_amount: Some(-881), - }; - assert_eq!(perp_market.amm.quote_asset_amount_with_unsettled_lp, 0); - let fee_to_market = 1000000; // uno doll - let liq_split = AMMLiquiditySplit::Shared; - let base_unit: i128 = perp_market.amm.get_per_lp_base_unit().unwrap(); - assert_eq!(base_unit, 1_000_000_000_000); // 10^4 * base_precision - - let (per_lp_delta_base, per_lp_delta_quote, per_lp_fee) = perp_market - .amm - .calculate_per_lp_delta(&pos_delta, fee_to_market, liq_split, base_unit) - .unwrap(); - - assert_eq!(per_lp_delta_base, -211759); - assert_eq!(per_lp_delta_quote, 31764); - assert_eq!(per_lp_fee, 169); - - let pos_delta2 = PositionDelta { - quote_asset_amount: -QUOTE_PRECISION_I64 * 150, - base_asset_amount: BASE_PRECISION_I64, - remainder_base_asset_amount: Some(0), - }; - let (per_lp_delta_base, per_lp_delta_quote, per_lp_fee) = perp_market - .amm - .calculate_per_lp_delta(&pos_delta2, fee_to_market, liq_split, base_unit) - .unwrap(); - - assert_eq!(per_lp_delta_base, 211759); - assert_eq!(per_lp_delta_quote, -31763); - assert_eq!(per_lp_fee, 169); - - let expected_base_asset_amount_with_unsettled_lp = -75249424409; - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - // 0 - expected_base_asset_amount_with_unsettled_lp // ~-75 - ); - // let lp_delta_quote = perp_market - // .amm - // .calculate_lp_base_delta(per_lp_delta_quote, QUOTE_PRECISION_I128) - // .unwrap(); - // assert_eq!(lp_delta_quote, -19883754464333); - - let delta_base = - update_lp_market_position(&mut perp_market, &pos_delta, fee_to_market, liq_split).unwrap(); - assert_eq!( - perp_market.amm.user_lp_shares * 1000000 / perp_market.amm.sqrt_k, - 132561 - ); // 13.2 % of amm - assert_eq!( - perp_market.amm.quote_asset_amount_with_unsettled_lp, - 19884380 - ); // 19.884380/.132 ~= 150 (+ fee) - assert_eq!(delta_base, -132_561_910); // ~13% - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - // 0 - -75381986319 // ~-75 - ); - - // settle lp and quote with unsettled should go back to zero - existing_position.lp_shares = perp_market.amm.user_lp_shares as u64; - existing_position.per_lp_base = 3; - - let lp_metrics: crate::math::lp::LPMetrics = - calculate_settle_lp_metrics(&perp_market.amm, &existing_position).unwrap(); - - let position_delta = PositionDelta { - base_asset_amount: lp_metrics.base_asset_amount as i64, - quote_asset_amount: lp_metrics.quote_asset_amount as i64, - remainder_base_asset_amount: Some(lp_metrics.remainder_base_asset_amount as i64), - }; - - assert_eq!(position_delta.base_asset_amount, 100000000); - - assert_eq!( - position_delta.remainder_base_asset_amount.unwrap_or(0), - 32561910 - ); - - assert_eq!(position_delta.quote_asset_amount, -19778585); - - let pnl = update_position_and_market(&mut existing_position, &mut perp_market, &position_delta) - .unwrap(); - - //.132561*1e6*.8 = 106048.8 - assert_eq!(perp_market.amm.quote_asset_amount_with_unsettled_lp, 105795); //? - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - expected_base_asset_amount_with_unsettled_lp - 32561910 - ); - - assert_eq!(pnl, 0); -} - -#[test] -fn amm_split_large_k_with_rebase() { - let perp_market_str = String::from("Ct8MLGv1N/dvAH3EF67yBqaUQerctpm4yqpK+QNSrXCQz76p+B+ka+8Ni2/aLOukHaFdQJXR2jkqDS+O0MbHvA9M+sjCgLVtQwhkAQAAAAAAAAAAAAAAAAIAAAAAAAAAkI1kAQAAAAB6XWQBAAAAAO8yzWQAAAAAnJ7I3f///////////////2dHvwAAAAAAAAAAAAAAAABGiVjX6roAAAAAAAAAAAAAAAAAAAAAAAB1tO47J+xiAAAAAAAAAAAAGD03Fis3mgAAAAAAAAAAAJxiDwAAAAAAAAAAAAAAAABxqRCIGRxiAAAAAAAAAAAAEy8wZfK9YwAAAAAAAAAAAGZeZCE+g3sAAAAAAAAAAAAKYeQAAAAAAAAAAAAAAAAAlIvoyyc3mgAAAAAAAAAAAADQdQKjbgAAAAAAAAAAAAAAwu8g05H/////////////E6tNHAIAAAAAAAAAAAAAAO3mFwd0AAAAAAAAAAAAAAAAgPQg5rUAAAAAAAAAAAAAGkDtXR4AAAAAAAAAAAAAAEv0WeZW/f////////////9kUidaqAIAAAAAAAAAAAAA0ZMEr1H9/////////////w5/U3uqAgAAAAAAAAAAAAAANfbqfCd3AAAAAAAAAAAAIhABAAAAAAAiEAEAAAAAACIQAQAAAAAAY1QBAAAAAAA5f3WMVAAAAAAAAAAAAAAAFhkiihsAAAAAAAAAAAAAAO2EfWc5AAAAAAAAAAAAAACM/5CAQgAAAAAAAAAAAAAAvenX0SsAAAAAAAAAAAAAALgPUogZAAAAAAAAAAAAAAC01x97AAAAAAAAAAAAAAAAOXzVbgAAAAAAAAAAAAAAAMG4+QwBAAAAAAAAAAAAAABwHI3fLeJiAAAAAAAAAAAABvigOblGmgAAAAAAAAAAALeRnZsi9mIAAAAAAAAAAAAqgs3ynCeaAAAAAAAAAAAAQwhkAQAAAAAAAAAAAAAAAJOMZAEAAAAAFKJkAQAAAABTl2QBAAAAALFuZAEAAAAAgrx7DAAAAAAUAwAAAAAAAAN1TAYAAAAAuC7NZAAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAA4fUFAAAAAAAAAAAAAAAAn2HvyMABAADGV6rZFwAAAE5Qg2oPAAAA8zHNZAAAAAAdYAAAAAAAAE2FAAAAAAAA6zLNZAAAAAD6AAAAaEIAABQDAAAUAwAAAAAAANcBAABkADIAZGQAAcDIUt4AAAAA0QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI9qQbynsAAAAAAAAAAAAAAAAAAAAAAAAFNPTC1QRVJQICAgICAgICAgICAgICAgICAgICAgICAghuS1//////8A4fUFAAAAAAB0O6QLAAAAR7PdeQMAAAD+Mc1kAAAAAADKmjsAAAAAAAAAAAAAAAAAAAAAAAAAAOULDwAAAAAAUBkAAAAAAADtAQAAAAAAAMgAAAAAAAAAECcAAKhhAADoAwAA9AEAAAAAAAAQJwAAZAIAAGQCAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let mut perp_market = perp_market_loader.load_mut().unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498335213293 - ); - - let og_baawul = perp_market.amm.base_asset_amount_with_unsettled_lp; - let og_baapl = perp_market.amm.base_asset_amount_per_lp; - let og_qaapl = perp_market.amm.quote_asset_amount_per_lp; - - // update base - let base_change = 5; - apply_lp_rebase_to_perp_market(&mut perp_market, base_change).unwrap(); - - // noop delta - let delta = PositionDelta { - base_asset_amount: 0, - quote_asset_amount: 0, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, og_qaapl * 100000); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, og_baapl * 100000); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - og_baawul - ); - - // min long order for $2.3 - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64 / 10, - quote_asset_amount: -2300000, - remainder_base_asset_amount: None, - }; - - let u1 = - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - assert_eq!(u1, 96471070); - - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498431684363 - ); - - assert_eq!( - perp_market.amm.base_asset_amount_per_lp - og_baapl * 100000, - -287639 - ); - assert_eq!( - perp_market.amm.quote_asset_amount_per_lp - og_qaapl * 100000, - 6615 - ); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp - og_baawul, - 96471070 - ); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -57405475887639); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 1253565506615); - - let num = perp_market.amm.quote_asset_amount_per_lp - (og_qaapl * 100000); - let denom = perp_market.amm.base_asset_amount_per_lp - (og_baapl * 100000); - assert_eq!(-num * 1000000 / denom, 22997); // $22.997 cost basis for short (vs $23 actual) - - // min short order for $2.3 - let delta = PositionDelta { - base_asset_amount: -BASE_PRECISION_I64 / 10, - quote_asset_amount: 2300000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -57405475600000); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 1253565499999); - assert_eq!( - (og_qaapl * 100000) - perp_market.amm.quote_asset_amount_per_lp, - 1 - ); - - let mut existing_position = PerpPosition { - market_index: 0, - base_asset_amount: 0, - quote_asset_amount: 0, - lp_shares: perp_market.amm.user_lp_shares as u64, - last_base_asset_amount_per_lp: og_baapl as i64, - last_quote_asset_amount_per_lp: og_qaapl as i64, - per_lp_base: 0, - ..PerpPosition::default() - }; - - settle_lp_position(&mut existing_position, &mut perp_market).unwrap(); - - assert_eq!(existing_position.base_asset_amount, 0); - assert_eq!(existing_position.remainder_base_asset_amount, 0); - assert_eq!(existing_position.quote_asset_amount, -335); // out of favor rounding... :/ - - assert_eq!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - assert_eq!( - perp_market - .amm - .imbalanced_base_asset_amount_with_lp() - .unwrap(), - -303686915482213 - ); - - assert_eq!(perp_market.amm.target_base_asset_amount_per_lp, -565000000); - - // update base back - let base_change = -2; - apply_lp_rebase_to_perp_market(&mut perp_market, base_change).unwrap(); - // noop delta - let delta = PositionDelta { - base_asset_amount: 0, - quote_asset_amount: 0, - remainder_base_asset_amount: None, - }; - - // unchanged with rebase (even when target!=0) - assert_eq!( - perp_market - .amm - .imbalanced_base_asset_amount_with_lp() - .unwrap(), - -303686915482213 - ); - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!( - perp_market.amm.quote_asset_amount_per_lp, - og_qaapl * 1000 - 1 - ); // down only rounding - assert_eq!(perp_market.amm.base_asset_amount_per_lp, og_baapl * 1000); - - // 1 long order for $23 before lp position does rebasing - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64, - quote_asset_amount: -23000000, - remainder_base_asset_amount: None, - }; - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756000); - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - let now = 110; - let clock_slot = 111; - let state = State::default(); - let oracle_price_data = OraclePriceData { - price: 23 * PRICE_PRECISION_I64, - confidence: PRICE_PRECISION_U64 / 100, - delay: 14, - has_sufficient_number_of_data_points: true, - }; - - let cost = _update_amm( - &mut perp_market, - &oracle_price_data, - &state, - now, - clock_slot, - ) - .unwrap(); - assert_eq!(cost, -3017938); - - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655660); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054784763); - assert_eq!( - existing_position.last_base_asset_amount_per_lp, - -57405475600000 - ); - assert_eq!(existing_position.per_lp_base, 5); - assert_ne!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - assert_eq!(perp_market.amm.base_asset_amount_long, 121646400000000); - assert_eq!(perp_market.amm.base_asset_amount_short, -121139000000000); - assert_eq!(perp_market.amm.base_asset_amount_with_amm, 8100106185); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 499299893815 - ); - let prev_with_unsettled_lp = perp_market.amm.base_asset_amount_with_unsettled_lp; - settle_lp_position(&mut existing_position, &mut perp_market).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_long, 121646400000000); - assert_eq!(perp_market.amm.base_asset_amount_short, -121139900000000); - assert_eq!(perp_market.amm.base_asset_amount_with_amm, 8100106185); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498399893815 - ); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498399893815 - ); - assert!(perp_market.amm.base_asset_amount_with_unsettled_lp < prev_with_unsettled_lp); - - // 96.47% owned - assert_eq!(perp_market.amm.user_lp_shares, 33538939700000000); - assert_eq!(perp_market.amm.sqrt_k, 34765725006847590); - - assert_eq!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - assert_eq!(existing_position.base_asset_amount, -900000000); - assert_eq!(existing_position.remainder_base_asset_amount, -64680522); - assert_eq!(existing_position.quote_asset_amount, 22168904); // out of favor rounding... :/ - assert_eq!( - existing_position.last_base_asset_amount_per_lp, - perp_market.amm.base_asset_amount_per_lp as i64 - ); // out of favor rounding... :/ - assert_eq!( - existing_position.last_quote_asset_amount_per_lp, - perp_market.amm.quote_asset_amount_per_lp as i64 - ); // out of favor rounding... :/ -} - -#[test] -fn full_lp_split() { - let delta = PositionDelta { - base_asset_amount: 10 * BASE_PRECISION_I64, - quote_asset_amount: -10 * BASE_PRECISION_I64, - remainder_base_asset_amount: None, - }; - - let amm = AMM { - user_lp_shares: 100 * AMM_RESERVE_PRECISION, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: 10 * AMM_RESERVE_PRECISION_I128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - update_lp_market_position(&mut market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!( - market.amm.base_asset_amount_per_lp as i64, - -10 * BASE_PRECISION_I64 / 100 - ); - assert_eq!( - market.amm.quote_asset_amount_per_lp as i64, - 10 * BASE_PRECISION_I64 / 100 - ); - assert_eq!(market.amm.base_asset_amount_with_amm, 0); - assert_eq!( - market.amm.base_asset_amount_with_unsettled_lp, - 10 * AMM_RESERVE_PRECISION_I128 - ); -} - -#[test] -fn half_half_amm_lp_split() { - let delta = PositionDelta { - base_asset_amount: 10 * BASE_PRECISION_I64, - quote_asset_amount: -10 * BASE_PRECISION_I64, - remainder_base_asset_amount: None, - }; - - let amm = AMM { - user_lp_shares: 100 * AMM_RESERVE_PRECISION, - sqrt_k: 200 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: 10 * AMM_RESERVE_PRECISION_I128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - update_lp_market_position(&mut market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!( - market.amm.base_asset_amount_with_amm, - 5 * AMM_RESERVE_PRECISION_I128 - ); - assert_eq!( - market.amm.base_asset_amount_with_unsettled_lp, - 5 * AMM_RESERVE_PRECISION_I128 - ); -} - #[test] fn test_position_entry_sim() { let mut existing_position: PerpPosition = PerpPosition::default(); let position_delta = PositionDelta { base_asset_amount: BASE_PRECISION_I64 / 2, quote_asset_amount: -99_345_000 / 2, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1288,7 +705,6 @@ fn test_position_entry_sim() { let position_delta_to_reduce = PositionDelta { base_asset_amount: -BASE_PRECISION_I64 / 5, quote_asset_amount: 99_245_000 / 5, - remainder_base_asset_amount: None, }; let pnl = update_position_and_market( @@ -1306,7 +722,6 @@ fn test_position_entry_sim() { let position_delta_to_flip = PositionDelta { base_asset_amount: -BASE_PRECISION_I64, quote_asset_amount: 99_345_000, - remainder_base_asset_amount: None, }; let pnl = @@ -1325,7 +740,6 @@ fn increase_long_from_no_position() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1365,7 +779,6 @@ fn increase_short_from_no_position() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1409,7 +822,6 @@ fn increase_long() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1461,7 +873,6 @@ fn increase_short() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1510,7 +921,6 @@ fn reduce_long_profitable() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1561,7 +971,6 @@ fn reduce_long_unprofitable() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1612,7 +1021,6 @@ fn flip_long_to_short_profitable() { let position_delta = PositionDelta { base_asset_amount: -11, quote_asset_amount: 22, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1664,7 +1072,6 @@ fn flip_long_to_short_unprofitable() { let position_delta = PositionDelta { base_asset_amount: -11, quote_asset_amount: 10, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1717,7 +1124,6 @@ fn reduce_short_profitable() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1766,7 +1172,6 @@ fn decrease_short_unprofitable() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1815,7 +1220,6 @@ fn flip_short_to_long_profitable() { let position_delta = PositionDelta { base_asset_amount: 11, quote_asset_amount: -60, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1867,7 +1271,6 @@ fn flip_short_to_long_unprofitable() { let position_delta = PositionDelta { base_asset_amount: 11, quote_asset_amount: -120, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1919,7 +1322,6 @@ fn close_long_profitable() { let position_delta = PositionDelta { base_asset_amount: -10, quote_asset_amount: 15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1970,7 +1372,6 @@ fn close_long_unprofitable() { let position_delta = PositionDelta { base_asset_amount: -10, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2020,7 +1421,6 @@ fn close_short_profitable() { let position_delta = PositionDelta { base_asset_amount: 10, quote_asset_amount: -5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2068,7 +1468,6 @@ fn close_short_unprofitable() { let position_delta = PositionDelta { base_asset_amount: 10, quote_asset_amount: -15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2116,7 +1515,6 @@ fn close_long_with_quote_break_even_amount_less_than_quote_asset_amount() { let position_delta = PositionDelta { base_asset_amount: -10, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2167,7 +1565,6 @@ fn close_short_with_quote_break_even_amount_more_than_quote_asset_amount() { let position_delta = PositionDelta { base_asset_amount: 10, quote_asset_amount: -15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index b5a8acb8f4..2510948d5d 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -3422,39 +3422,6 @@ pub fn handle_update_perp_market_target_base_asset_amount_per_lp( Ok(()) } -pub fn handle_update_perp_market_per_lp_base( - ctx: Context, - per_lp_base: i8, -) -> Result<()> { - let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; - msg!("perp market {}", perp_market.market_index); - - let old_per_lp_base = perp_market.amm.per_lp_base; - msg!( - "updated perp_market per_lp_base {} -> {}", - old_per_lp_base, - per_lp_base - ); - - let expo_diff = per_lp_base.safe_sub(old_per_lp_base)?; - - validate!( - expo_diff.abs() == 1, - ErrorCode::DefaultError, - "invalid expo update (must be 1)", - )?; - - validate!( - per_lp_base.abs() <= 9, - ErrorCode::DefaultError, - "only consider lp_base within range of AMM_RESERVE_PRECISION", - )?; - - controller::lp::apply_lp_rebase_to_perp_market(perp_market, expo_diff)?; - - Ok(()) -} - pub fn handle_update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 4b00898041..222ec30755 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1077,37 +1077,6 @@ pub fn handle_settle_funding_payment<'c: 'info, 'info>( Ok(()) } -#[access_control( - amm_not_paused(&ctx.accounts.state) -)] -pub fn handle_settle_lp<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, SettleLP>, - market_index: u16, -) -> Result<()> { - let user_key = ctx.accounts.user.key(); - let user = &mut load_mut!(ctx.accounts.user)?; - - let state = &ctx.accounts.state; - let clock = Clock::get()?; - let now = clock.unix_timestamp; - - let AccountMaps { - perp_market_map, .. - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - let market = &mut perp_market_map.get_ref_mut(&market_index)?; - controller::lp::settle_funding_payment_then_lp(user, &user_key, market, now)?; - user.update_last_active_slot(clock.slot); - - Ok(()) -} - #[access_control( liq_not_paused(&ctx.accounts.state) )] diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 3717db74cc..80c3d7c013 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -11,6 +11,7 @@ use anchor_spl::{ use solana_program::program::invoke; use solana_program::system_instruction::transfer; +use crate::controller::funding::settle_funding_payment; use crate::controller::orders::{cancel_orders, ModifyOrderId}; use crate::controller::position::update_position_and_market; use crate::controller::position::PositionDirection; @@ -1611,14 +1612,14 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( &clock, )?; - controller::lp::settle_funding_payment_then_lp( + settle_funding_payment( &mut from_user, &from_user_key, perp_market_map.get_ref_mut(&market_index)?.deref_mut(), now, )?; - controller::lp::settle_funding_payment_then_lp( + settle_funding_payment( &mut to_user, &to_user_key, perp_market_map.get_ref_mut(&market_index)?.deref_mut(), @@ -2923,208 +2924,6 @@ pub fn handle_place_and_make_spot_order<'c: 'info, 'info>( Ok(()) } -#[access_control( - amm_not_paused(&ctx.accounts.state) -)] -pub fn handle_add_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - n_shares: u64, - market_index: u16, -) -> Result<()> { - let user_key = ctx.accounts.user.key(); - let user = &mut load_mut!(ctx.accounts.user)?; - let state = &ctx.accounts.state; - let clock = Clock::get()?; - let now = clock.unix_timestamp; - - msg!("add_perp_lp_shares is disabled"); - return Err(ErrorCode::DefaultError.into()); - - let AccountMaps { - perp_market_map, - spot_market_map, - mut oracle_map, - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; - math::liquidation::validate_user_not_being_liquidated( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - state.liquidation_margin_buffer_ratio, - )?; - - { - let mut market = perp_market_map.get_ref_mut(&market_index)?; - - validate!( - matches!(market.status, MarketStatus::Active), - ErrorCode::MarketStatusInvalidForNewLP, - "Market Status doesn't allow for new LP liquidity" - )?; - - validate!( - !matches!(market.contract_type, ContractType::Prediction), - ErrorCode::MarketStatusInvalidForNewLP, - "Contract Type doesn't allow for LP liquidity" - )?; - - validate!( - !market.is_operation_paused(PerpOperation::AmmFill), - ErrorCode::MarketStatusInvalidForNewLP, - "Market amm fills paused" - )?; - - validate!( - n_shares >= market.amm.order_step_size, - ErrorCode::NewLPSizeTooSmall, - "minting {} shares is less than step size {}", - n_shares, - market.amm.order_step_size, - )?; - - controller::funding::settle_funding_payment(user, &user_key, &mut market, now)?; - - // standardize n shares to mint - let n_shares = crate::math::orders::standardize_base_asset_amount( - n_shares.cast()?, - market.amm.order_step_size, - )? - .cast::()?; - - controller::lp::mint_lp_shares( - user.force_get_perp_position_mut(market_index)?, - &mut market, - n_shares, - )?; - - user.last_add_perp_lp_shares_ts = now; - } - - // check margin requirements - meets_place_order_margin_requirement( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - true, - )?; - - user.update_last_active_slot(clock.slot); - - emit!(LPRecord { - ts: now, - action: LPAction::AddLiquidity, - user: user_key, - n_shares, - market_index, - ..LPRecord::default() - }); - - Ok(()) -} - -pub fn handle_remove_perp_lp_shares_in_expiring_market<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, RemoveLiquidityInExpiredMarket<'info>>, - shares_to_burn: u64, - market_index: u16, -) -> Result<()> { - let user_key = ctx.accounts.user.key(); - let user = &mut load_mut!(ctx.accounts.user)?; - - let state = &ctx.accounts.state; - let clock = Clock::get()?; - let now = clock.unix_timestamp; - - let AccountMaps { - perp_market_map, - mut oracle_map, - .. - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - // additional validate - { - let signer_is_admin = ctx.accounts.signer.key() == admin_hot_wallet::id(); - let market = perp_market_map.get_ref(&market_index)?; - validate!( - market.is_reduce_only()? || signer_is_admin, - ErrorCode::PerpMarketNotInReduceOnly, - "Can only permissionless burn when market is in reduce only" - )?; - } - - controller::lp::remove_perp_lp_shares( - perp_market_map, - &mut oracle_map, - state, - user, - user_key, - shares_to_burn, - market_index, - now, - )?; - - user.update_last_active_slot(clock.slot); - - Ok(()) -} - -#[access_control( - amm_not_paused(&ctx.accounts.state) -)] -pub fn handle_remove_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - shares_to_burn: u64, - market_index: u16, -) -> Result<()> { - let user_key = ctx.accounts.user.key(); - let user = &mut load_mut!(ctx.accounts.user)?; - - let state = &ctx.accounts.state; - let clock = Clock::get()?; - let now = clock.unix_timestamp; - - let AccountMaps { - perp_market_map, - mut oracle_map, - .. - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - controller::lp::remove_perp_lp_shares( - perp_market_map, - &mut oracle_map, - state, - user, - user_key, - shares_to_burn, - market_index, - now, - )?; - - user.update_last_active_slot(clock.slot); - - Ok(()) -} - pub fn handle_update_user_name( ctx: Context, _sub_account_id: u16, @@ -4618,25 +4417,6 @@ pub struct PlaceAndMatchRFQOrders<'info> { pub ix_sysvar: AccountInfo<'info>, } -#[derive(Accounts)] -pub struct AddRemoveLiquidity<'info> { - pub state: Box>, - #[account( - mut, - constraint = can_sign_for_user(&user, &authority)?, - )] - pub user: AccountLoader<'info, User>, - pub authority: Signer<'info>, -} - -#[derive(Accounts)] -pub struct RemoveLiquidityInExpiredMarket<'info> { - pub state: Box>, - #[account(mut)] - pub user: AccountLoader<'info, User>, - pub signer: Signer<'info>, -} - #[derive(Accounts)] #[instruction( sub_account_id: u16, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 9deb3ceca9..92981feac5 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -324,30 +324,6 @@ pub mod drift { ) } - pub fn add_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - n_shares: u64, - market_index: u16, - ) -> Result<()> { - handle_add_perp_lp_shares(ctx, n_shares, market_index) - } - - pub fn remove_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - shares_to_burn: u64, - market_index: u16, - ) -> Result<()> { - handle_remove_perp_lp_shares(ctx, shares_to_burn, market_index) - } - - pub fn remove_perp_lp_shares_in_expiring_market<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, RemoveLiquidityInExpiredMarket<'info>>, - shares_to_burn: u64, - market_index: u16, - ) -> Result<()> { - handle_remove_perp_lp_shares_in_expiring_market(ctx, shares_to_burn, market_index) - } - pub fn update_user_name( ctx: Context, _sub_account_id: u16, @@ -539,13 +515,6 @@ pub mod drift { handle_settle_funding_payment(ctx) } - pub fn settle_lp<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, SettleLP>, - market_index: u16, - ) -> Result<()> { - handle_settle_lp(ctx, market_index) - } - pub fn settle_expired_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AdminUpdatePerpMarket<'info>>, market_index: u16, @@ -1372,13 +1341,6 @@ pub mod drift { ) } - pub fn update_perp_market_per_lp_base( - ctx: Context, - per_lp_base: i8, - ) -> Result<()> { - handle_update_perp_market_per_lp_base(ctx, per_lp_base) - } - pub fn update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 400c6504e8..287b103060 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -22,7 +22,6 @@ pub fn is_user_bankrupt(user: &User) -> bool { if perp_position.base_asset_amount != 0 || perp_position.quote_asset_amount > 0 || perp_position.has_open_order() - || perp_position.is_lp() { return false; } diff --git a/programs/drift/src/math/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index 99039b67d0..2ae3d2506b 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -1,7 +1,4 @@ use crate::controller::amm::update_spreads; -use crate::controller::lp::burn_lp_shares; -use crate::controller::lp::mint_lp_shares; -use crate::controller::lp::settle_lp_position; use crate::controller::position::PositionDirection; use crate::math::amm::calculate_bid_ask_bounds; use crate::math::constants::BASE_PRECISION; @@ -338,7 +335,7 @@ fn amm_spread_adj_logic() { ..PerpPosition::default() }; - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); + // todo fix this market.amm.base_asset_amount_per_lp = 1; market.amm.quote_asset_amount_per_lp = -QUOTE_PRECISION_I64 as i128; @@ -368,174 +365,4 @@ fn amm_spread_adj_logic() { update_spreads(&mut market, reserve_price).unwrap(); assert_eq!(market.amm.long_spread, 110); assert_eq!(market.amm.short_spread, 110); -} - -#[test] -fn calculate_k_with_lps_tests() { - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - terminal_quote_asset_reserve: 999900009999000 * AMM_RESERVE_PRECISION, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 50_000_000_000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 10) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 10) as i128, - order_step_size: 5, - max_spread: 1000, - ..AMM::default_test() - }, - margin_ratio_initial: 1000, - ..PerpMarket::default() - }; - // let (t_price, _t_qar, _t_bar) = calculate_terminal_price_and_reserves(&market.amm).unwrap(); - // market.amm.terminal_quote_asset_reserve = _t_qar; - - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 1; - market.amm.quote_asset_amount_per_lp = -QUOTE_PRECISION_I64 as i128; - - let reserve_price = market.amm.reserve_price().unwrap(); - update_spreads(&mut market, reserve_price).unwrap(); - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -QUOTE_PRECISION_I64); - assert_eq!(position.last_base_asset_amount_per_lp, 1); - assert_eq!( - position.last_quote_asset_amount_per_lp, - -QUOTE_PRECISION_I64 - ); - - // increase k by 1% - let update_k_up = - get_update_k_result(&market, bn::U192::from(102 * AMM_RESERVE_PRECISION), false).unwrap(); - let (t_price, _t_qar, _t_bar) = - amm::calculate_terminal_price_and_reserves(&market.amm).unwrap(); - - // new terminal reserves are balanced, terminal price = peg) - // assert_eq!(t_qar, 999900009999000); - // assert_eq!(t_bar, 1000100000000000); - assert_eq!(t_price, 49901136949); // - // assert_eq!(update_k_up.sqrt_k, 101 * AMM_RESERVE_PRECISION); - - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - ); - assert_eq!(cost, 49400); //0.05 - - // lp whale adds - let lp_whale_amount = 1000 * BASE_PRECISION_U64; - mint_lp_shares(&mut position, &mut market, lp_whale_amount).unwrap(); - - // ensure same cost - let update_k_up = - get_update_k_result(&market, bn::U192::from(1102 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - ); - assert_eq!(cost, 49450); //0.05 - - let update_k_down = - get_update_k_result(&market, bn::U192::from(1001 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_down).unwrap(); - assert_eq!(cost, -4995004950); //amm rug - - // lp whale removes - burn_lp_shares(&mut position, &mut market, lp_whale_amount, 0).unwrap(); - - // ensure same cost - let update_k_up = - get_update_k_result(&market, bn::U192::from(102 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - 1 - ); - assert_eq!(cost, 49450); //0.05 - - let update_k_down = - get_update_k_result(&market, bn::U192::from(79 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_down).unwrap(); - assert_eq!(cost, -1407000); //0.05 - - // lp owns 50% of vAMM, same k - position.lp_shares = 50 * BASE_PRECISION_U64; - market.amm.user_lp_shares = 50 * AMM_RESERVE_PRECISION; - // cost to increase k is always positive when imbalanced - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - 1 - ); - assert_eq!(cost, 187800); //0.19 - - // lp owns 99% of vAMM, same k - position.lp_shares = 99 * BASE_PRECISION_U64; - market.amm.user_lp_shares = 99 * AMM_RESERVE_PRECISION; - let cost2 = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert!(cost2 > cost); - assert_eq!(cost2, 76804900); //216.45 - - // lp owns 100% of vAMM, same k - position.lp_shares = 100 * BASE_PRECISION_U64; - market.amm.user_lp_shares = 100 * AMM_RESERVE_PRECISION; - let cost3 = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert!(cost3 > cost); - assert!(cost3 > cost2); - assert_eq!(cost3, 216450200); - - // // todo: support this - // market.amm.base_asset_amount_with_amm = -(AMM_RESERVE_PRECISION as i128); - // let cost2 = adjust_k_cost(&mut market, &update_k_up).unwrap(); - // assert!(cost2 > cost); - // assert_eq!(cost2, 249999999999850000000001); -} - -#[test] -fn calculate_bid_ask_per_lp_token() { - let (bound1_s, bound2_s) = - calculate_bid_ask_bounds(MAX_CONCENTRATION_COEFFICIENT, 24704615072091).unwrap(); - - assert_eq!(bound1_s, 17468968372288); - assert_eq!(bound2_s, 34937266634951); - - let (bound1, bound2) = calculate_bid_ask_bounds( - MAX_CONCENTRATION_COEFFICIENT, - 24704615072091 + BASE_PRECISION, - ) - .unwrap(); - - assert_eq!(bound1 - bound1_s, 707113563); - assert_eq!(bound2 - bound2_s, 1414200000); - - let more_conc = - CONCENTRATION_PRECISION + (MAX_CONCENTRATION_COEFFICIENT - CONCENTRATION_PRECISION) / 20; - - let (bound1_s, bound2_s) = calculate_bid_ask_bounds(more_conc, 24704615072091).unwrap(); - - assert_eq!(bound1_s, 24203363415750); - assert_eq!(bound2_s, 25216247650234); - - let (bound1, bound2) = - calculate_bid_ask_bounds(more_conc, 24704615072091 + BASE_PRECISION).unwrap(); - - assert_eq!(bound1 - bound1_s, 979710202); - assert_eq!(bound2 - bound2_s, 1020710000); - - let (bound1_3, bound2_3) = - calculate_bid_ask_bounds(more_conc, 24704615072091 + 2 * BASE_PRECISION).unwrap(); - - assert_eq!(bound1_3 - bound1_s, 979710202 * 2); - assert_eq!(bound2_3 - bound2_s, 1020710000 * 2); -} +} \ No newline at end of file diff --git a/programs/drift/src/math/lp.rs b/programs/drift/src/math/lp.rs deleted file mode 100644 index a65ae33adb..0000000000 --- a/programs/drift/src/math/lp.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::error::{DriftResult, ErrorCode}; -use crate::msg; -use crate::{ - validate, MARGIN_PRECISION_U128, PRICE_PRECISION, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, -}; -use std::u64; - -use crate::math::amm::calculate_market_open_bids_asks; -use crate::math::casting::Cast; -use crate::math::helpers; -use crate::math::margin::MarginRequirementType; -use crate::math::orders::{ - standardize_base_asset_amount, standardize_base_asset_amount_ceil, - standardize_base_asset_amount_with_remainder_i128, -}; -use crate::math::safe_math::SafeMath; - -use crate::state::perp_market::PerpMarket; -use crate::state::perp_market::AMM; -use crate::state::user::PerpPosition; - -#[cfg(test)] -mod tests; - -#[derive(Debug)] -pub struct LPMetrics { - pub base_asset_amount: i128, - pub quote_asset_amount: i128, - pub remainder_base_asset_amount: i128, -} - -pub fn calculate_settle_lp_metrics(amm: &AMM, position: &PerpPosition) -> DriftResult { - let (base_asset_amount, quote_asset_amount) = calculate_settled_lp_base_quote(amm, position)?; - - // stepsize it - let (standardized_base_asset_amount, remainder_base_asset_amount) = - standardize_base_asset_amount_with_remainder_i128( - base_asset_amount, - amm.order_step_size.cast()?, - )?; - - let lp_metrics = LPMetrics { - base_asset_amount: standardized_base_asset_amount, - quote_asset_amount, - remainder_base_asset_amount: remainder_base_asset_amount.cast()?, - }; - - Ok(lp_metrics) -} - -pub fn calculate_settled_lp_base_quote( - amm: &AMM, - position: &PerpPosition, -) -> DriftResult<(i128, i128)> { - let n_shares = position.lp_shares; - let base_unit: i128 = amm.get_per_lp_base_unit()?; - - validate!( - amm.per_lp_base == position.per_lp_base, - ErrorCode::InvalidPerpPositionDetected, - "calculate_settled_lp_base_quote :: position/market per_lp_base unequal {} != {}", - position.per_lp_base, - amm.per_lp_base - )?; - - let n_shares_i128 = n_shares.cast::()?; - - // give them slice of the damm market position - let amm_net_base_asset_amount_per_lp = amm - .base_asset_amount_per_lp - .safe_sub(position.last_base_asset_amount_per_lp.cast()?)?; - - let base_asset_amount = amm_net_base_asset_amount_per_lp - .cast::()? - .safe_mul(n_shares_i128)? - .safe_div(base_unit)?; - - let amm_net_quote_asset_amount_per_lp = amm - .quote_asset_amount_per_lp - .safe_sub(position.last_quote_asset_amount_per_lp.cast()?)?; - - let quote_asset_amount = amm_net_quote_asset_amount_per_lp - .cast::()? - .safe_mul(n_shares_i128)? - .safe_div(base_unit)?; - - Ok((base_asset_amount, quote_asset_amount)) -} - -pub fn calculate_lp_open_bids_asks( - market_position: &PerpPosition, - market: &PerpMarket, -) -> DriftResult<(i64, i64)> { - let total_lp_shares = market.amm.sqrt_k; - let lp_shares = market_position.lp_shares; - - let (max_bids, max_asks) = calculate_market_open_bids_asks(&market.amm)?; - let open_asks = helpers::get_proportion_i128(max_asks, lp_shares.cast()?, total_lp_shares)?; - let open_bids = helpers::get_proportion_i128(max_bids, lp_shares.cast()?, total_lp_shares)?; - - Ok((open_bids.cast()?, open_asks.cast()?)) -} - -pub fn calculate_lp_shares_to_burn_for_risk_reduction( - perp_position: &PerpPosition, - market: &PerpMarket, - oracle_price: i64, - quote_oracle_price: i64, - margin_shortage: u128, - user_custom_margin_ratio: u32, - user_high_leverage_mode: bool, -) -> DriftResult<(u64, u64)> { - let settled_lp_position = perp_position.simulate_settled_lp_position(market, oracle_price)?; - - let worse_case_base_asset_amount = - settled_lp_position.worst_case_base_asset_amount(oracle_price, market.contract_type)?; - - let open_orders_from_lp_shares = if worse_case_base_asset_amount >= 0 { - worse_case_base_asset_amount.safe_sub( - settled_lp_position - .base_asset_amount - .safe_add(perp_position.open_bids)? - .cast()?, - )? - } else { - worse_case_base_asset_amount.safe_sub( - settled_lp_position - .base_asset_amount - .safe_add(perp_position.open_asks)? - .cast()?, - )? - }; - - let margin_ratio = market - .get_margin_ratio( - worse_case_base_asset_amount.unsigned_abs(), - MarginRequirementType::Initial, - user_high_leverage_mode, - )? - .max(user_custom_margin_ratio); - - let base_asset_amount_to_cover = margin_shortage - .safe_mul(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO)? - .safe_div( - oracle_price - .cast::()? - .safe_mul(quote_oracle_price.cast()?)? - .safe_div(PRICE_PRECISION)? - .safe_mul(margin_ratio.cast()?)? - .safe_div(MARGIN_PRECISION_U128)?, - )? - .cast::()?; - - let current_base_asset_amount = settled_lp_position.base_asset_amount.unsigned_abs(); - - // if closing position is enough to cover margin shortage, then only a small % of lp shares need to be burned - if base_asset_amount_to_cover < current_base_asset_amount { - let base_asset_amount_to_close = standardize_base_asset_amount_ceil( - base_asset_amount_to_cover, - market.amm.order_step_size, - )? - .min(current_base_asset_amount); - let lp_shares_to_burn = standardize_base_asset_amount( - settled_lp_position.lp_shares / 10, - market.amm.order_step_size, - )? - .max(market.amm.order_step_size); - return Ok((lp_shares_to_burn, base_asset_amount_to_close)); - } - - let base_asset_amount_to_cover = - base_asset_amount_to_cover.safe_sub(current_base_asset_amount)?; - - let percent_to_burn = base_asset_amount_to_cover - .cast::()? - .safe_mul(100)? - .safe_div_ceil(open_orders_from_lp_shares.unsigned_abs())?; - - let lp_shares_to_burn = settled_lp_position - .lp_shares - .cast::()? - .safe_mul(percent_to_burn.cast()?)? - .safe_div_ceil(100)? - .cast::()?; - - let standardized_lp_shares_to_burn = - standardize_base_asset_amount_ceil(lp_shares_to_burn, market.amm.order_step_size)? - .clamp(market.amm.order_step_size, settled_lp_position.lp_shares); - - Ok((standardized_lp_shares_to_burn, current_base_asset_amount)) -} diff --git a/programs/drift/src/math/lp/tests.rs b/programs/drift/src/math/lp/tests.rs deleted file mode 100644 index c55253e0a5..0000000000 --- a/programs/drift/src/math/lp/tests.rs +++ /dev/null @@ -1,451 +0,0 @@ -use crate::math::constants::AMM_RESERVE_PRECISION; -use crate::math::lp::*; -use crate::state::user::PerpPosition; - -mod calculate_get_proportion_u128 { - use crate::math::helpers::get_proportion_u128; - - use super::*; - - pub fn get_proportion_u128_safe( - value: u128, - numerator: u128, - denominator: u128, - ) -> DriftResult { - if numerator == 0 { - return Ok(0); - } - - let proportional_value = if numerator <= denominator { - let ratio = denominator.safe_mul(10000)?.safe_div(numerator)?; - value.safe_mul(10000)?.safe_div(ratio)? - } else { - value.safe_mul(numerator)?.safe_div(denominator)? - }; - - Ok(proportional_value) - } - - #[test] - fn test_safe() { - let sqrt_k = AMM_RESERVE_PRECISION * 10_123; - let max_reserve = sqrt_k * 14121 / 10000; - let max_asks = max_reserve - sqrt_k; - - // let ans1 = get_proportion_u128_safe(max_asks, sqrt_k - sqrt_k / 100, sqrt_k).unwrap(); - // let ans2 = get_proportion_u128(max_asks, sqrt_k - sqrt_k / 100, sqrt_k).unwrap(); - // assert_eq!(ans1, ans2); //fails - - let ans1 = get_proportion_u128_safe(max_asks, sqrt_k / 2, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, sqrt_k / 2, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - let ans1 = get_proportion_u128_safe(max_asks, AMM_RESERVE_PRECISION, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, AMM_RESERVE_PRECISION, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - let ans1 = get_proportion_u128_safe(max_asks, 0, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, 0, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - let ans1 = get_proportion_u128_safe(max_asks, 1325324, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, 1325324, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - // let ans1 = get_proportion_u128(max_asks, sqrt_k, sqrt_k).unwrap(); - // assert_eq!(ans1, max_asks); - } -} - -mod calculate_lp_open_bids_asks { - use super::*; - - #[test] - fn test_simple_lp_bid_ask() { - let position = PerpPosition { - lp_shares: 100, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_reserve: 10, - max_base_asset_reserve: 100, - min_base_asset_reserve: 0, - sqrt_k: 200, - ..AMM::default_test() - }; - let market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - let (open_bids, open_asks) = calculate_lp_open_bids_asks(&position, &market).unwrap(); - - assert_eq!(open_bids, 10 * 100 / 200); - assert_eq!(open_asks, -90 * 100 / 200); - } - - #[test] - fn test_max_ask() { - let position = PerpPosition { - lp_shares: 100, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_reserve: 0, - max_base_asset_reserve: 100, - min_base_asset_reserve: 0, - sqrt_k: 200, - ..AMM::default_test() - }; - let market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - let (open_bids, open_asks) = calculate_lp_open_bids_asks(&position, &market).unwrap(); - - assert_eq!(open_bids, 0); // wont go anymore short - assert_eq!(open_asks, -100 * 100 / 200); - } - - #[test] - fn test_max_bid() { - let position = PerpPosition { - lp_shares: 100, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_reserve: 10, - max_base_asset_reserve: 10, - min_base_asset_reserve: 0, - sqrt_k: 200, - ..AMM::default_test() - }; - let market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - let (open_bids, open_asks) = calculate_lp_open_bids_asks(&position, &market).unwrap(); - - assert_eq!(open_bids, 10 * 100 / 200); - assert_eq!(open_asks, 0); // no more long - } -} - -mod calculate_settled_lp_base_quote { - use crate::math::constants::BASE_PRECISION_U64; - - use super::*; - - #[test] - fn test_long_settle() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - ..AMM::default_test() - }; - - let (baa, qaa) = calculate_settled_lp_base_quote(&amm, &position).unwrap(); - - assert_eq!(baa, 10 * 100); - assert_eq!(qaa, -10 * 100); - } - - #[test] - fn test_short_settle() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: -10, - quote_asset_amount_per_lp: 10, - ..AMM::default_test() - }; - - let (baa, qaa) = calculate_settled_lp_base_quote(&amm, &position).unwrap(); - - assert_eq!(baa, -10 * 100); - assert_eq!(qaa, 10 * 100); - } -} - -mod calculate_settle_lp_metrics { - use crate::math::constants::BASE_PRECISION_U64; - - use super::*; - - #[test] - fn test_long_settle() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - order_step_size: 1, - ..AMM::default_test() - }; - - let lp_metrics = calculate_settle_lp_metrics(&amm, &position).unwrap(); - - assert_eq!(lp_metrics.base_asset_amount, 10 * 100); - assert_eq!(lp_metrics.quote_asset_amount, -10 * 100); - assert_eq!(lp_metrics.remainder_base_asset_amount, 0); - } - - #[test] - fn test_all_remainder() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - order_step_size: 50 * 100, - ..AMM::default_test() - }; - - let lp_metrics = calculate_settle_lp_metrics(&amm, &position).unwrap(); - - assert_eq!(lp_metrics.base_asset_amount, 0); - assert_eq!(lp_metrics.quote_asset_amount, -10 * 100); - assert_eq!(lp_metrics.remainder_base_asset_amount, 10 * 100); - } - - #[test] - fn test_portion_remainder() { - let position = PerpPosition { - lp_shares: BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - order_step_size: 3, - ..AMM::default_test() - }; - - let lp_metrics = calculate_settle_lp_metrics(&amm, &position).unwrap(); - - assert_eq!(lp_metrics.base_asset_amount, 9); - assert_eq!(lp_metrics.quote_asset_amount, -10); - assert_eq!(lp_metrics.remainder_base_asset_amount, 1); - } -} - -mod calculate_lp_shares_to_burn_for_risk_reduction { - use crate::math::lp::calculate_lp_shares_to_burn_for_risk_reduction; - use crate::state::perp_market::PerpMarket; - use crate::state::user::User; - use crate::test_utils::create_account_info; - use crate::{PRICE_PRECISION_I64, QUOTE_PRECISION}; - use anchor_lang::prelude::AccountLoader; - use solana_program::pubkey::Pubkey; - use std::str::FromStr; - - #[test] - fn test() { - let user_str = String::from("n3Vf4++XOuwuqzjlmLoHfrMxu0bx1zK4CI3jhlcn84aSUBauaSLU4gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARHJpZnQgTGlxdWlkaXR5IFByb3ZpZGVyICAgICAgICAbACHcCQAAAAAAAAAAAAAAAAAAAAAAAACcpMgCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2ssqwBAAAAAATnHP3///+9awAIAAAAAKnr8AcAAAAAqufxBwAAAAAAAAAAAAAAAAAAAAAAAAAAuITI//////8AeTlTJwAAANxGF1tu/P//abUakBEAAACBFNL6BAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+hUCAAAAAAAAAAAAAAAAACC8EHuk9f//1uYrCQMAAAAAAAAACQAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANv7p2UAAAAAnKTIAgAAAAAAAAAAAAAAAAAAAAAAAAAAsprK//////8AAAAAAAAAAPeGAgAAAAAAAAAAAAAAAAAzkaIOAAAAAA8AAACIEwAAAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - let mut decoded_bytes = base64::decode(user_str).unwrap(); - let user_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let user_account_info = create_account_info(&key, true, &mut lamports, user_bytes, &owner); - - let user_loader: AccountLoader = AccountLoader::try_from(&user_account_info).unwrap(); - let mut user = user_loader.load_mut().unwrap(); - let position = &mut user.perp_positions[0]; - - let perp_market_str = String::from("Ct8MLGv1N/cU6tVVkVpIHdjrXil5+Blo7M7no01SEzFkvCN2nSnel3KwISF8o/5okioZqvmQEJy52E6a0AS00gJa1vUpMUQZgG2jAAAAAAAAAAAAAAAAAAMAAAAAAAAAiKOiAAAAAAATRqMAAAAAAEr2u2UAAAAA3EYXW278/////////////2m1GpARAAAAAAAAAAAAAACRgrV0qi0BAAAAAAAAAAAAAAAAAAAAAABFREBhQ1YEAAAAAAAAAAAA9sh+SuuHBwAAAAAAAAAAACaTDwAAAAAAAAAAAAAAAADvHx32D0IEAAAAAAAAAAAA67nFJa5vBAAAAAAAAAAAAHMxOUELtwUAAAAAAAAAAACqHV4AAAAAAAAAAAAAAAAApw4iE86DBwAAAAAAAAAAAADzSoISXwAAAAAAAAAAAAAAHtBmbKP/////////////CreY1F8CAAAAAAAAAAAAAPZZghQfAAAAAAAAAAAAAAAAQGNSv8YBAAAAAAAAAAAAUdkndDAAAAAAAAAAAAAAAEEeAcSS/v/////////////0bAXnbQEAAAAAAAAAAAAAPuj0I3f+/////////////6felr+KAQAAAAAAAAAAAABX2/mMhMQCAAAAAAAAAAAALukbAAAAAAAu6RsAAAAAAC7pGwAAAAAAqPUJAAAAAADkPmeWogAAAAAAAAAAAAAAsD8vhpIAAAAAAAAAAAAAACibCEwQAAAAAAAAAAAAAAAr/d/xbQAAAAAAAAAAAAAAwY+XFgAAAAAAAAAAAAAAAMyF/KFFAAAAAAAAAAAAAAA9rLKsAQAAAAAAAAAAAAAAPayyrAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+6JzGf04EAAAAAAAAAAAAtqLk+X6VBwAAAAAAAAAAAPDUDdGDVwQAAAAAAAAAAABeb5d+v4UHAAAAAAAAAAAAgG2jAAAAAAAAAAAAAAAAACJ6ogAAAAAAE0qkAAAAAAAaYqMAAAAAAIF1pAAAAAAArJmiDgAAAAAlBwAAAAAAAN5ukP7/////veq7ZQAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAAZc0dAAAAAAAAAAAAAAAAiuqcc0QAAAA8R6NuAQAAAIyqSgkAAAAAt+27ZQAAAAATCAEAAAAAAPjJAAAAAAAASva7ZQAAAACUEQAAoIYBALQ2AADKCAAASQEAAH0AAAD0ATIAZMgEAQAAAAAEAAAAfRuiDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhv4EJ8hQEAAAAAAAAAAAAAAAAAAAAAADFNQk9OSy1QRVJQICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG8VAwAAAAAA+x4AAAAAAACFAwAAAAAAACYCAADuAgAAqGEAAFDDAADECQAA3AUAAAAAAAAQJwAABwQAAA0GAAAEAAEAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let perp_market = perp_market_loader.load_mut().unwrap(); - - let oracle_price = 10 * PRICE_PRECISION_I64; - let quote_oracle_price = PRICE_PRECISION_I64; - - let margin_shortage = 40 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 168900000000); - assert_eq!(base_asset_amount, 12400000000); - - let margin_shortage = 20 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 8000000000); - - let margin_shortage = 5 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 2000000000); - - // flip existing position the other direction - position.base_asset_amount = -position.base_asset_amount; - - let margin_shortage = 40 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 168900000000); - assert_eq!(base_asset_amount, 12400000000); - - let margin_shortage = 20 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 8000000000); - - let margin_shortage = 5 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 2000000000); - } - - #[test] - fn custom_margin_ratio() { - let user_str = String::from("n3Vf4++XOuwIrD1jL22rz6RZlEfmZHqxneDBS0Mflxjd93h2f2ldQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdGl0c29jY2VyICAgICAgICAgICAgICAgICAgICAgICDnqurCZBgAAAAAAAAAAAAAAAAAAAAAAADOM8akAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgO0UfBAAAAPeaGv//////SM9HIAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyRaK3v////8AAAAAAAAAAPeaGv//////SM9HIAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGq0wmUAAAAATspepQEAAACAlpgAAAAAAAAAAAAAAAAAGn3imQQAAAAAAAAAAAAAACMV2Pf/////AAAAAAAAAAB7Ro0QAAAAACYAAAAQJwAAAQADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - let mut decoded_bytes = base64::decode(user_str).unwrap(); - let user_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let user_account_info = create_account_info(&key, true, &mut lamports, user_bytes, &owner); - - let user_loader: AccountLoader = AccountLoader::try_from(&user_account_info).unwrap(); - let mut user = user_loader.load_mut().unwrap(); - let user_custom_margin_ratio = user.max_margin_ratio; - let position = &mut user.perp_positions[0]; - - let perp_market_str = String::from("Ct8MLGv1N/cV6vWLwJY+18dY2GsrmrNldgnISB7pmbcf7cn9S4FZ4PnAFyuhDfpNGQiNlPW/YdO1TVvXSDoyKpguE3PujqMbEGDwDQoAAAAAAAAAAAAAAAIAAAAAAAAAC7rzDQoAAABst/MNCgAAACK5wmUAAAAAceZN/////////////////3v2pRcAAAAAAAAAAAAAAADlXWMfRwMAAAAAAAAAAAAAAAAAAAAAAABkI6UNRQAAAAAAAAAAAAAAqfLzd0UAAAAAAAAAAAAAACaTDwAAAAAAAAAAAAAAAAAPPu6ERAAAAAAAAAAAAAAA9NX/YkcAAAAAAAAAAAAAAI0luEJFAAAAAAAAAAAAAAD9eI3+CQAAAAAAAAAAAAAAIZTqlkQAAAAAAAAAAAAAAIBENlMCAAAAAAAAAAAAAADgfcyL/v//////////////meiO4gAAAAAAAAAAAAAAAMfZc/z///////////////8AoHJOGAkAAAAAAAAAAAAAnURpyQAAAAAAAAAAAAAAAOYK1g3F//////////////9I7emQMwAAAAAAAAAAAAAAKoCi9MT//////////////3cTS98zAAAAAAAAAAAAAAAgO0UfBAAAAAAAAAAAAAAAGV4SEgAAAAAZXhISAAAAABleEhIAAAAAA0itQgAAAAAhNkO9CQAAAAAAAAAAAAAAMTlM9gcAAAAAAAAAAAAAAGJujdoBAAAAAAAAAAAAAADvEtDaAgAAAAAAAAAAAAAAOLU20gIAAAAAAAAAAAAAALu3wLsCAAAAAAAAAAAAAABdkcJWBgUAAAAAAAAAAAAANhdFnLwEAAAAAAAAAAAAAEDj5IkCAAAAAAAAAAAAAACCV2gFRQAAAAAAAAAAAAAAPWo+gEUAAAAAAAAAAAAAAIIDPw5FAAAAAAAAAAAAAAAAJ1l3RQAAAAAAAAAAAAAAEGDwDQoAAAAAAAAAAAAAAE05yg0KAAAAUbrzDQoAAADP+d4NCgAAAC3a3g0KAAAAGVONEAAAAAACAQAAAAAAAGDUKbz/////G7nCZQAAAAAQDgAAAAAAAKCGAQAAAAAAoIYBAAAAAABAQg8AAAAAAAAAAAAAAAAA3+0VvzsAAAAAAAAAAAAAACbWIwUKAAAAIbnCZQAAAABNyBIAAAAAAPtZAwAAAAAAIbnCZQAAAAAKAAAA6AMAAKQDAABEAAAAAAAAAP0pAABkADIAZMgAAQDKmjsAAAAA1jQGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ezvzkkgAAAAAAAAAAAAAAAAAAAAAAAEJUQy1QRVJQICAgICAgICAgICAgICAgICAgICAgICAg6AMAAAAAAADoAwAAAAAAAOgDAAAAAAAA6AMAAAAAAAAiucJlAAAAABAnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJsXAwAAAAAAIhAAAAAAAACHAQAAAAAAABAnAAAQJwAAECcAABAnAAD0AQAAkAEAAAAAAAABAAAAFwAAABsAAAABAAEAAgAAALX/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let perp_market = perp_market_loader.load_mut().unwrap(); - - let oracle_price = 43174 * PRICE_PRECISION_I64; - let quote_oracle_price = PRICE_PRECISION_I64; - - let margin_shortage = 2077 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - user_custom_margin_ratio, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 1770400000); - assert_eq!(base_asset_amount, 48200000); - assert_eq!(position.lp_shares, 17704500000); - } -} diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 73415cb58b..cc6af96add 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -119,8 +119,6 @@ pub fn calculate_perp_position_value_and_pnl( market_position, )?; - let market_position = market_position.simulate_settled_lp_position(market, valuation_price)?; - let (base_asset_value, unrealized_pnl) = calculate_base_asset_value_and_pnl_with_oracle_price(&market_position, valuation_price)?; @@ -150,11 +148,7 @@ pub fn calculate_perp_position_value_and_pnl( // add small margin requirement for every open order margin_requirement = margin_requirement - .safe_add(market_position.margin_requirement_for_open_orders()?)? - .safe_add( - market_position - .margin_requirement_for_lp_shares(market.amm.order_step_size, valuation_price)?, - )?; + .safe_add(market_position.margin_requirement_for_open_orders()?)?; let unrealized_asset_weight = market.get_unrealized_asset_weight(total_unrealized_pnl, margin_requirement_type)?; @@ -584,8 +578,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( let has_perp_liability = market_position.base_asset_amount != 0 || market_position.quote_asset_amount < 0 - || market_position.has_open_order() - || market_position.is_lp(); + || market_position.has_open_order(); if has_perp_liability { calculation.add_perp_liability()?; @@ -978,9 +971,6 @@ pub fn calculate_user_equity( market_position, )?; - let market_position = - market_position.simulate_settled_lp_position(market, valuation_price)?; - let (_, unrealized_pnl) = calculate_base_asset_value_and_pnl_with_oracle_price( &market_position, valuation_price, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 33763abd05..7a256b65db 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -394,154 +394,6 @@ mod test { let ans = (0).nth_root(2); assert_eq!(ans, 0); } - - #[test] - fn test_lp_user_short() { - let mut market = PerpMarket { - market_index: 0, - amm: AMM { - base_asset_reserve: 5 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 5 * AMM_RESERVE_PRECISION, - sqrt_k: 5 * AMM_RESERVE_PRECISION, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, - max_base_asset_reserve: 10 * AMM_RESERVE_PRECISION, - ..AMM::default_test() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - imf_factor: 1000, // 1_000/1_000_000 = .001 - unrealized_pnl_initial_asset_weight: 10000, - unrealized_pnl_maintenance_asset_weight: 10000, - ..PerpMarket::default() - }; - - let position = PerpPosition { - lp_shares: market.amm.user_lp_shares as u64, - ..PerpPosition::default() - }; - - let oracle_price_data = OraclePriceData { - price: (2 * PRICE_PRECISION) as i64, - confidence: 0, - delay: 2, - has_sufficient_number_of_data_points: true, - }; - - let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // make the market unbalanced - - let trade_size = 3 * AMM_RESERVE_PRECISION; - let (new_qar, new_bar) = calculate_swap_output( - trade_size, - market.amm.base_asset_reserve, - SwapDirection::Add, // user shorts - market.amm.sqrt_k, - ) - .unwrap(); - market.amm.quote_asset_reserve = new_qar; - market.amm.base_asset_reserve = new_bar; - - let (pmr2, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // larger margin req in more unbalanced market - assert!(pmr2 > pmr) - } - - #[test] - fn test_lp_user_long() { - let mut market = PerpMarket { - market_index: 0, - amm: AMM { - base_asset_reserve: 5 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 5 * AMM_RESERVE_PRECISION, - sqrt_k: 5 * AMM_RESERVE_PRECISION, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, - max_base_asset_reserve: 10 * AMM_RESERVE_PRECISION, - ..AMM::default_test() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - imf_factor: 1000, // 1_000/1_000_000 = .001 - unrealized_pnl_initial_asset_weight: 10000, - unrealized_pnl_maintenance_asset_weight: 10000, - ..PerpMarket::default() - }; - - let position = PerpPosition { - lp_shares: market.amm.user_lp_shares as u64, - ..PerpPosition::default() - }; - - let oracle_price_data = OraclePriceData { - price: (2 * PRICE_PRECISION) as i64, - confidence: 0, - delay: 2, - has_sufficient_number_of_data_points: true, - }; - - let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // make the market unbalanced - let trade_size = 3 * AMM_RESERVE_PRECISION; - let (new_qar, new_bar) = calculate_swap_output( - trade_size, - market.amm.base_asset_reserve, - SwapDirection::Remove, // user longs - market.amm.sqrt_k, - ) - .unwrap(); - market.amm.quote_asset_reserve = new_qar; - market.amm.base_asset_reserve = new_bar; - - let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr2, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // larger margin req in more unbalanced market - assert!(pmr2 > pmr) - } } #[cfg(test)] diff --git a/programs/drift/src/math/mod.rs b/programs/drift/src/math/mod.rs index aa7ec7f196..89edbdafc5 100644 --- a/programs/drift/src/math/mod.rs +++ b/programs/drift/src/math/mod.rs @@ -16,7 +16,6 @@ pub mod funding; pub mod helpers; pub mod insurance; pub mod liquidation; -pub mod lp; pub mod margin; pub mod matching; pub mod oracle; diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 749b2efada..ec146bd1f7 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -353,7 +353,6 @@ pub fn get_position_delta_for_fill( PositionDirection::Long => base_asset_amount.cast()?, PositionDirection::Short => -base_asset_amount.cast()?, }, - remainder_base_asset_amount: None, }) } diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index b16c7c1c34..b07f7c91c7 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -174,32 +174,19 @@ pub fn get_position_update_type( position: &PerpPosition, delta: &PositionDelta, ) -> DriftResult { - if position.base_asset_amount == 0 && position.remainder_base_asset_amount == 0 { + if position.base_asset_amount == 0 { return Ok(PositionUpdateType::Open); } - let position_base_with_remainder = if position.remainder_base_asset_amount != 0 { - position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount.cast::()?)? - } else { - position.base_asset_amount - }; + let position_base = position.base_asset_amount; - let delta_base_with_remainder = - if let Some(remainder_base_asset_amount) = delta.remainder_base_asset_amount { - delta - .base_asset_amount - .safe_add(remainder_base_asset_amount.cast()?)? - } else { - delta.base_asset_amount - }; + let delta_base = delta.base_asset_amount; - if position_base_with_remainder.signum() == delta_base_with_remainder.signum() { + if position_base.signum() == delta_base.signum() { Ok(PositionUpdateType::Increase) - } else if position_base_with_remainder.abs() > delta_base_with_remainder.abs() { + } else if position_base.abs() > delta_base.abs() { Ok(PositionUpdateType::Reduce) - } else if position_base_with_remainder.abs() == delta_base_with_remainder.abs() { + } else if position_base.abs() == delta_base.abs() { Ok(PositionUpdateType::Close) } else { Ok(PositionUpdateType::Flip) @@ -209,8 +196,7 @@ pub fn get_position_update_type( pub fn get_new_position_amounts( position: &PerpPosition, delta: &PositionDelta, - market: &PerpMarket, -) -> DriftResult<(i64, i64, i64, i64)> { +) -> DriftResult<(i64, i64)> { let new_quote_asset_amount = position .quote_asset_amount .safe_add(delta.quote_asset_amount)?; @@ -219,48 +205,8 @@ pub fn get_new_position_amounts( .base_asset_amount .safe_add(delta.base_asset_amount)?; - let mut new_remainder_base_asset_amount = position - .remainder_base_asset_amount - .cast::()? - .safe_add( - delta - .remainder_base_asset_amount - .unwrap_or(0) - .cast::()?, - )?; - let mut new_settled_base_asset_amount = delta.base_asset_amount; - - if delta.remainder_base_asset_amount.is_some() { - if new_remainder_base_asset_amount.unsigned_abs() >= market.amm.order_step_size { - let (standardized_remainder_base_asset_amount, remainder_base_asset_amount) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - new_remainder_base_asset_amount.cast()?, - market.amm.order_step_size.cast()?, - )?; - - new_base_asset_amount = - new_base_asset_amount.safe_add(standardized_remainder_base_asset_amount.cast()?)?; - - new_settled_base_asset_amount = new_settled_base_asset_amount - .safe_add(standardized_remainder_base_asset_amount.cast()?)?; - - new_remainder_base_asset_amount = remainder_base_asset_amount.cast()?; - } else { - new_remainder_base_asset_amount = new_remainder_base_asset_amount.cast()?; - } - - validate!( - new_remainder_base_asset_amount.abs() <= i32::MAX as i64, - ErrorCode::InvalidPositionDelta, - "new_remainder_base_asset_amount={} > i32 max", - new_remainder_base_asset_amount - )?; - } - Ok(( new_base_asset_amount, - new_settled_base_asset_amount, new_quote_asset_amount, - new_remainder_base_asset_amount, )) } diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 8983162b7b..b4f18c1e2c 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -589,29 +589,6 @@ impl PerpMarket { Ok(depth) } - pub fn update_market_with_counterparty( - &mut self, - delta: &PositionDelta, - new_settled_base_asset_amount: i64, - ) -> DriftResult { - // indicates that position delta is settling lp counterparty - if delta.remainder_base_asset_amount.is_some() { - // todo: name for this is confusing, but adding is correct as is - // definition: net position of users in the market that has the LP as a counterparty (which have NOT settled) - self.amm.base_asset_amount_with_unsettled_lp = self - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(new_settled_base_asset_amount.cast()?)?; - - self.amm.quote_asset_amount_with_unsettled_lp = self - .amm - .quote_asset_amount_with_unsettled_lp - .safe_add(delta.quote_asset_amount.cast()?)?; - } - - Ok(()) - } - pub fn is_price_divergence_ok_for_settle_pnl(&self, oracle_price: i64) -> DriftResult { let oracle_divergence = oracle_price .safe_sub(self.amm.historical_oracle_data.last_oracle_price_twap_5min)? diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 2f49d86889..b32292bdbc 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1,4 +1,3 @@ -use crate::controller::lp::apply_lp_rebase_to_perp_position; use crate::controller::position::{add_new_position, get_position_index, PositionDirection}; use crate::error::{DriftResult, ErrorCode}; use crate::math::auction::{calculate_auction_price, is_auction_complete}; @@ -7,14 +6,13 @@ use crate::math::constants::{ EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, OPEN_ORDER_MARGIN_REQUIREMENT, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, THIRTY_DAY, }; -use crate::math::lp::{calculate_lp_open_bids_asks, calculate_settle_lp_metrics}; use crate::math::margin::MarginRequirementType; use crate::math::orders::{ apply_protected_maker_limit_price_offset, standardize_base_asset_amount, standardize_price, }; use crate::math::position::{ calculate_base_asset_value_and_pnl_with_oracle_price, - calculate_base_asset_value_with_oracle_price, calculate_perp_liability_value, + calculate_perp_liability_value, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{ @@ -23,10 +21,10 @@ use crate::math::spot_balance::{ use crate::math::stats::calculate_rolling_sum; use crate::msg; use crate::state::oracle::StrictOraclePrice; -use crate::state::perp_market::{ContractType, PerpMarket}; +use crate::state::perp_market::{ContractType}; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::traits::Size; -use crate::{get_then_update_id, ID, PERCENTAGE_PRECISION_I64, QUOTE_PRECISION_U64}; +use crate::{get_then_update_id, ID, QUOTE_PRECISION_U64}; use crate::{math_error, SPOT_WEIGHT_PRECISION_I128}; use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; @@ -979,7 +977,6 @@ impl PerpPosition { !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() - && !self.is_lp() } pub fn is_open_position(&self) -> bool { @@ -990,103 +987,12 @@ impl PerpPosition { self.open_orders != 0 || self.open_bids != 0 || self.open_asks != 0 } - pub fn margin_requirement_for_lp_shares( - &self, - order_step_size: u64, - valuation_price: i64, - ) -> DriftResult { - if !self.is_lp() { - return Ok(0); - } - Ok(QUOTE_PRECISION.max( - order_step_size - .cast::()? - .safe_mul(valuation_price.cast()?)? - .safe_div(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO)?, - )) - } - pub fn margin_requirement_for_open_orders(&self) -> DriftResult { self.open_orders .cast::()? .safe_mul(OPEN_ORDER_MARGIN_REQUIREMENT) } - pub fn is_lp(&self) -> bool { - self.lp_shares > 0 - } - - pub fn simulate_settled_lp_position( - &self, - market: &PerpMarket, - valuation_price: i64, - ) -> DriftResult { - let mut settled_position = *self; - - if !settled_position.is_lp() { - return Ok(settled_position); - } - - apply_lp_rebase_to_perp_position(market, &mut settled_position)?; - - // compute lp metrics - let mut lp_metrics = calculate_settle_lp_metrics(&market.amm, &settled_position)?; - - // compute settled position - let base_asset_amount = settled_position - .base_asset_amount - .safe_add(lp_metrics.base_asset_amount.cast()?)?; - - let mut quote_asset_amount = settled_position - .quote_asset_amount - .safe_add(lp_metrics.quote_asset_amount.cast()?)?; - - let mut new_remainder_base_asset_amount = settled_position - .remainder_base_asset_amount - .cast::()? - .safe_add(lp_metrics.remainder_base_asset_amount.cast()?)?; - - if new_remainder_base_asset_amount.unsigned_abs() >= market.amm.order_step_size { - let (standardized_remainder_base_asset_amount, remainder_base_asset_amount) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - new_remainder_base_asset_amount.cast()?, - market.amm.order_step_size.cast()?, - )?; - - lp_metrics.base_asset_amount = lp_metrics - .base_asset_amount - .safe_add(standardized_remainder_base_asset_amount)?; - - new_remainder_base_asset_amount = remainder_base_asset_amount.cast()?; - } else { - new_remainder_base_asset_amount = new_remainder_base_asset_amount.cast()?; - } - - // dust position in baa/qaa - if new_remainder_base_asset_amount != 0 { - let dust_base_asset_value = calculate_base_asset_value_with_oracle_price( - new_remainder_base_asset_amount.cast()?, - valuation_price, - )? - .safe_add(1)?; - - quote_asset_amount = quote_asset_amount.safe_sub(dust_base_asset_value.cast()?)?; - } - - let (lp_bids, lp_asks) = calculate_lp_open_bids_asks(&settled_position, market)?; - - let open_bids = settled_position.open_bids.safe_add(lp_bids)?; - - let open_asks = settled_position.open_asks.safe_add(lp_asks)?; - - settled_position.base_asset_amount = base_asset_amount; - settled_position.quote_asset_amount = quote_asset_amount; - settled_position.open_bids = open_bids; - settled_position.open_asks = open_asks; - - Ok(settled_position) - } - pub fn has_unsettled_pnl(&self) -> bool { self.base_asset_amount == 0 && self.quote_asset_amount != 0 } @@ -1162,18 +1068,12 @@ impl PerpPosition { Ok(unrealized_pnl) } - pub fn get_base_asset_amount_with_remainder(&self) -> DriftResult { - if self.remainder_base_asset_amount != 0 { - self.base_asset_amount - .cast::()? - .safe_add(self.remainder_base_asset_amount.cast::()?) - } else { - self.base_asset_amount.cast::() - } + pub fn get_base_asset_amount(&self) -> DriftResult { + self.base_asset_amount.cast::() } - pub fn get_base_asset_amount_with_remainder_abs(&self) -> DriftResult { - Ok(self.get_base_asset_amount_with_remainder()?.abs()) + pub fn get_base_asset_amount_abs(&self) -> DriftResult { + Ok(self.get_base_asset_amount()?.abs()) } pub fn get_claimable_pnl(&self, oracle_price: i64, pnl_pool_excess: i128) -> DriftResult { @@ -1231,7 +1131,7 @@ use super::protected_maker_mode_config::ProtectedMakerParams; #[cfg(test)] impl PerpPosition { pub fn get_breakeven_price(&self) -> DriftResult { - let base_with_remainder = self.get_base_asset_amount_with_remainder()?; + let base_with_remainder = self.get_base_asset_amount()?; if base_with_remainder == 0 { return Ok(0); } @@ -1243,7 +1143,7 @@ impl PerpPosition { } pub fn get_entry_price(&self) -> DriftResult { - let base_with_remainder = self.get_base_asset_amount_with_remainder()?; + let base_with_remainder = self.get_base_asset_amount()?; if base_with_remainder == 0 { return Ok(0); } diff --git a/programs/drift/src/validation/position.rs b/programs/drift/src/validation/position.rs index e8f527de63..3d36698583 100644 --- a/programs/drift/src/validation/position.rs +++ b/programs/drift/src/validation/position.rs @@ -11,14 +11,6 @@ pub fn validate_perp_position_with_perp_market( position: &PerpPosition, market: &PerpMarket, ) -> DriftResult { - if position.lp_shares != 0 { - validate!( - position.per_lp_base == market.amm.per_lp_base, - ErrorCode::InvalidPerpPositionDetected, - "position/market per_lp_base unequal" - )?; - } - validate!( position.market_index == market.market_index, ErrorCode::InvalidPerpPositionDetected, From e99ffa7e9ecdccc6ada82e3d0fab8f5785d45c35 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 16:50:46 -0400 Subject: [PATCH 003/159] rm more fields --- programs/drift/src/controller/amm.rs | 3 +- programs/drift/src/controller/amm/tests.rs | 55 ------- programs/drift/src/controller/orders.rs | 4 +- .../src/controller/orders/amm_lp_jit_tests.rs | 136 ---------------- programs/drift/src/controller/repeg.rs | 2 +- programs/drift/src/instructions/admin.rs | 28 ---- programs/drift/src/lib.rs | 10 -- programs/drift/src/math/amm_jit.rs | 47 +----- programs/drift/src/math/funding.rs | 3 +- programs/drift/src/math/position.rs | 12 +- programs/drift/src/state/perp_market.rs | 146 +----------------- programs/drift/src/validation/perp_market.rs | 32 +--- 12 files changed, 19 insertions(+), 459 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 9aa33e087e..5e4871b8a3 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -360,8 +360,7 @@ pub fn formulaic_update_k( let new_sqrt_k = bn::U192::from(market.amm.sqrt_k) .safe_mul(bn::U192::from(k_scale_numerator))? - .safe_div(bn::U192::from(k_scale_denominator))? - .max(bn::U192::from(market.amm.user_lp_shares.safe_add(1)?)); + .safe_div(bn::U192::from(k_scale_denominator))?; let update_k_result = get_update_k_result(market, new_sqrt_k, true)?; diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index c62e2959d5..5031a75bbc 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -255,61 +255,6 @@ fn iterative_no_bounds_formualic_k_tests() { assert_eq!(market.amm.total_fee_minus_distributions, 985625029); } -#[test] -fn decrease_k_up_to_user_lp_shares() { - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 512295081967, - quote_asset_reserve: 488 * AMM_RESERVE_PRECISION, - sqrt_k: 500 * AMM_RESERVE_PRECISION, - user_lp_shares: 150 * AMM_RESERVE_PRECISION, - peg_multiplier: 50000000, - concentration_coef: MAX_CONCENTRATION_COEFFICIENT, - base_asset_amount_with_amm: -12295081967, - total_fee_minus_distributions: -100 * QUOTE_PRECISION as i128, - total_fee_withdrawn: 100 * QUOTE_PRECISION, - curve_update_intensity: 100, - ..AMM::default() - }, - ..PerpMarket::default() - }; - // let prev_sqrt_k = market.amm.sqrt_k; - let (new_terminal_quote_reserve, new_terminal_base_reserve) = - amm::calculate_terminal_reserves(&market.amm).unwrap(); - market.amm.terminal_quote_asset_reserve = new_terminal_quote_reserve; - let (min_base_asset_reserve, max_base_asset_reserve) = - amm::calculate_bid_ask_bounds(market.amm.concentration_coef, new_terminal_base_reserve) - .unwrap(); - market.amm.min_base_asset_reserve = min_base_asset_reserve; - market.amm.max_base_asset_reserve = max_base_asset_reserve; - - // let reserve_price = market.amm.reserve_price().unwrap(); - let now = 10000; - let oracle_price_data = OraclePriceData { - price: 50 * PRICE_PRECISION_I64, - confidence: 0, - delay: 2, - has_sufficient_number_of_data_points: true, - }; - - // negative funding cost - let mut count = 0; - let mut prev_k = market.amm.sqrt_k; - let mut new_k = 0; - while prev_k != new_k && count < 100000 { - let funding_cost = (QUOTE_PRECISION * 100000) as i128; - prev_k = market.amm.sqrt_k; - formulaic_update_k(&mut market, &oracle_price_data, funding_cost, now).unwrap(); - new_k = market.amm.sqrt_k; - msg!("quote_asset_reserve:{}", market.amm.quote_asset_reserve); - msg!("new_k:{}", new_k); - count += 1 - } - - assert_eq!(market.amm.base_asset_amount_with_amm, -12295081967); - assert_eq!(market.amm.sqrt_k, 162234889619); - assert_eq!(market.amm.total_fee_minus_distributions, 29796232175); -} #[test] fn update_pool_balances_test_high_util_borrow() { diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 8884c1695e..4502129845 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1092,6 +1092,7 @@ pub fn fill_perp_order( amm_lp_allowed_to_jit_make = market .amm .amm_lp_allowed_to_jit_make(amm_wants_to_jit_make)?; + // TODO what do do here? amm_can_skip_duration = market.can_skip_auction_duration(&state, amm_lp_allowed_to_jit_make)?; @@ -1862,7 +1863,6 @@ fn fulfill_perp_order( fee_structure, oracle_map, fill_mode.is_liquidation(), - None, )?; if maker_fill_base_asset_amount != 0 { @@ -2462,7 +2462,6 @@ pub fn fulfill_perp_order_with_match( fee_structure: &FeeStructure, oracle_map: &mut OracleMap, is_liquidation: bool, - amm_lp_allowed_to_jit_make: Option, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2549,7 +2548,6 @@ pub fn fulfill_perp_order_with_match( taker_base_asset_amount, maker_base_asset_amount, taker.orders[taker_order_index].has_limit_price(slot)?, - amm_lp_allowed_to_jit_make, )?; if jit_base_asset_amount > 0 { diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs index 2ef6a02bb7..d4f40df218 100644 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs @@ -326,146 +326,10 @@ pub mod amm_lp_jit { BASE_PRECISION_U64, BASE_PRECISION_U64, false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::ProtocolOwned); - assert_eq!(jit_base_asset_amount, 500000000); - } - - #[test] - fn amm_lp_jit_amm_lp_same_side_imbalanced() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, // lps are long vs target, wants shorts - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), // amm is too long vs target, wants shorts - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - // lp needs nearly 5 base to get to target - assert_eq!( - market.amm.imbalanced_base_asset_amount_with_lp().unwrap(), - 4_941_986_570 - ); - - 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; - - let amm_inventory_pct = calculate_inventory_liquidity_ratio( - market.amm.base_asset_amount_with_amm, - market.amm.base_asset_reserve, - market.amm.min_base_asset_reserve, - market.amm.max_base_asset_reserve, - ) - .unwrap(); - assert_eq!(amm_inventory_pct, PERCENTAGE_PRECISION_I128 / 200); // .5% of amm inventory is in position - - // maker order satisfies taker, vAMM doing match - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Long, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64, - BASE_PRECISION_U64, - false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::Shared); - assert_eq!(jit_base_asset_amount, 500000000); - - // taker order is heading to vAMM - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Long, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64 * 2, - BASE_PRECISION_U64, - false, - None, ) .unwrap(); assert_eq!(amm_liquidity_split, AMMLiquiditySplit::ProtocolOwned); - assert_eq!(jit_base_asset_amount, 0); // its coming anyways - - // no jit for additional long (more shorts for amm) - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Long, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64 * 100, - BASE_PRECISION_U64 * 100, - false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::Shared); assert_eq!(jit_base_asset_amount, 500000000); - - // wrong direction (increases lp and vamm inventory) - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Short, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64, - BASE_PRECISION_U64, - false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::ProtocolOwned); - assert_eq!(jit_base_asset_amount, 0); } #[test] diff --git a/programs/drift/src/controller/repeg.rs b/programs/drift/src/controller/repeg.rs index a9f55fe107..ea3af12285 100644 --- a/programs/drift/src/controller/repeg.rs +++ b/programs/drift/src/controller/repeg.rs @@ -333,7 +333,7 @@ pub fn settle_expired_market( )?; validate!( - market.amm.base_asset_amount_with_unsettled_lp == 0 && market.amm.user_lp_shares == 0, + market.amm.base_asset_amount_with_unsettled_lp == 0, ErrorCode::MarketSettlementRequiresSettledLP, "Outstanding LP in market" )?; diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 2510948d5d..669a6d95b2 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -2321,14 +2321,6 @@ pub fn handle_update_k(ctx: Context, sqrt_k: u128) -> Result<()> { perp_market.amm.sqrt_k )?; - validate!( - perp_market.amm.sqrt_k > perp_market.amm.user_lp_shares, - ErrorCode::InvalidUpdateK, - "cannot decrease sqrt_k={} below user_lp_shares={}", - perp_market.amm.sqrt_k, - perp_market.amm.user_lp_shares - )?; - perp_market.amm.total_fee_minus_distributions = perp_market .amm .total_fee_minus_distributions @@ -3402,26 +3394,6 @@ pub fn handle_update_perp_market_curve_update_intensity( Ok(()) } -#[access_control( - perp_market_valid(&ctx.accounts.perp_market) -)] -pub fn handle_update_perp_market_target_base_asset_amount_per_lp( - ctx: Context, - target_base_asset_amount_per_lp: i32, -) -> Result<()> { - let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; - msg!("perp market {}", perp_market.market_index); - - msg!( - "perp_market.amm.target_base_asset_amount_per_lp: {} -> {}", - perp_market.amm.target_base_asset_amount_per_lp, - target_base_asset_amount_per_lp - ); - - perp_market.amm.target_base_asset_amount_per_lp = target_base_asset_amount_per_lp; - Ok(()) -} - pub fn handle_update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 92981feac5..cedcbfbfeb 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1331,16 +1331,6 @@ pub mod drift { handle_update_perp_market_curve_update_intensity(ctx, curve_update_intensity) } - pub fn update_perp_market_target_base_asset_amount_per_lp( - ctx: Context, - target_base_asset_amount_per_lp: i32, - ) -> Result<()> { - handle_update_perp_market_target_base_asset_amount_per_lp( - ctx, - target_base_asset_amount_per_lp, - ) - } - pub fn update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/math/amm_jit.rs b/programs/drift/src/math/amm_jit.rs index 17179b24b3..8614402ab9 100644 --- a/programs/drift/src/math/amm_jit.rs +++ b/programs/drift/src/math/amm_jit.rs @@ -133,19 +133,11 @@ pub fn calculate_clamped_jit_base_asset_amount( .cast::()?; // bound it; dont flip the net_baa - let max_amm_base_asset_amount = if liquidity_split != AMMLiquiditySplit::LPOwned { - market - .amm - .base_asset_amount_with_amm - .unsigned_abs() - .cast::()? - } else { - market - .amm - .imbalanced_base_asset_amount_with_lp()? - .unsigned_abs() - .cast::()? - }; + let max_amm_base_asset_amount = market + .amm + .base_asset_amount_with_amm + .unsigned_abs() + .cast::()?; let jit_base_asset_amount = jit_base_asset_amount.min(max_amm_base_asset_amount); @@ -161,10 +153,9 @@ pub fn calculate_amm_jit_liquidity( taker_base_asset_amount: u64, maker_base_asset_amount: u64, taker_has_limit_price: bool, - amm_lp_allowed_to_jit_make: Option, ) -> DriftResult<(u64, AMMLiquiditySplit)> { let mut jit_base_asset_amount: u64 = 0; - let mut liquidity_split: AMMLiquiditySplit = AMMLiquiditySplit::ProtocolOwned; + let liquidity_split: AMMLiquiditySplit = AMMLiquiditySplit::ProtocolOwned; // taker has_limit_price = false means (limit price = 0 AND auction is complete) so // market order will always land and fill on amm next round @@ -177,33 +168,7 @@ pub fn calculate_amm_jit_liquidity( } let amm_wants_to_jit_make = market.amm.amm_wants_to_jit_make(taker_direction)?; - let amm_lp_wants_to_jit_make = market.amm.amm_lp_wants_to_jit_make(taker_direction)?; - let amm_lp_allowed_to_jit_make = match amm_lp_allowed_to_jit_make { - Some(allowed) => allowed, - None => market - .amm - .amm_lp_allowed_to_jit_make(amm_wants_to_jit_make)?, - }; - let split_with_lps = amm_lp_allowed_to_jit_make && amm_lp_wants_to_jit_make; - if amm_wants_to_jit_make { - liquidity_split = if split_with_lps { - AMMLiquiditySplit::Shared - } else { - AMMLiquiditySplit::ProtocolOwned - }; - - jit_base_asset_amount = calculate_jit_base_asset_amount( - market, - base_asset_amount, - maker_price, - valid_oracle_price, - taker_direction, - liquidity_split, - )?; - } else if split_with_lps { - liquidity_split = AMMLiquiditySplit::LPOwned; - jit_base_asset_amount = calculate_jit_base_asset_amount( market, base_asset_amount, diff --git a/programs/drift/src/math/funding.rs b/programs/drift/src/math/funding.rs index 5fc9d78a3a..683f62c2cc 100644 --- a/programs/drift/src/math/funding.rs +++ b/programs/drift/src/math/funding.rs @@ -29,8 +29,7 @@ pub fn calculate_funding_rate_long_short( // If the net market position owes funding payment, the protocol receives payment let settled_net_market_position = market .amm - .base_asset_amount_with_amm - .safe_add(market.amm.base_asset_amount_with_unsettled_lp)?; + .base_asset_amount_with_amm; let net_market_position_funding_payment = calculate_funding_payment_in_quote_precision(funding_rate, settled_net_market_position)?; diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index b07f7c91c7..8f008c0f35 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -43,23 +43,17 @@ pub fn calculate_base_asset_value(base_asset_amount: i128, amm: &AMM) -> DriftRe let (base_asset_reserve, quote_asset_reserve) = (amm.base_asset_reserve, amm.quote_asset_reserve); - let amm_lp_shares = amm.sqrt_k.safe_sub(amm.user_lp_shares)?; - - let base_asset_reserve_proportion = - get_proportion_u128(base_asset_reserve, amm_lp_shares, amm.sqrt_k)?; - - let quote_asset_reserve_proportion = - get_proportion_u128(quote_asset_reserve, amm_lp_shares, amm.sqrt_k)?; + let amm_lp_shares = amm.sqrt_k; let (new_quote_asset_reserve, _new_base_asset_reserve) = amm::calculate_swap_output( base_asset_amount.unsigned_abs(), - base_asset_reserve_proportion, + base_asset_reserve, swap_direction, amm_lp_shares, )?; let base_asset_value = calculate_quote_asset_amount_swapped( - quote_asset_reserve_proportion, + quote_asset_reserve, new_quote_asset_reserve, swap_direction, amm.peg_multiplier, diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index b4f18c1e2c..1adf030388 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1185,18 +1185,11 @@ impl AMM { } pub fn get_lower_bound_sqrt_k(self) -> DriftResult { - Ok(self.sqrt_k.min( - self.user_lp_shares - .safe_add(self.user_lp_shares.safe_div(1000)?)? - .max(self.min_order_size.cast()?) - .max(self.base_asset_amount_with_amm.unsigned_abs().cast()?), - )) + Ok(self.sqrt_k) } pub fn get_protocol_owned_position(self) -> DriftResult { - self.base_asset_amount_with_amm - .safe_add(self.base_asset_amount_with_unsettled_lp)? - .cast::() + self.base_asset_amount_with_amm.cast::() } pub fn get_max_reference_price_offset(self) -> DriftResult { @@ -1215,109 +1208,6 @@ impl AMM { Ok(max_offset) } - pub fn get_per_lp_base_unit(self) -> DriftResult { - let scalar: i128 = 10_i128.pow(self.per_lp_base.abs().cast()?); - - if self.per_lp_base > 0 { - AMM_RESERVE_PRECISION_I128.safe_mul(scalar) - } else { - AMM_RESERVE_PRECISION_I128.safe_div(scalar) - } - } - - pub fn calculate_lp_base_delta( - &self, - per_lp_delta_base: i128, - base_unit: i128, - ) -> DriftResult { - // calculate dedicated for user lp shares - let lp_delta_base = - get_proportion_i128(per_lp_delta_base, self.user_lp_shares, base_unit.cast()?)?; - - Ok(lp_delta_base) - } - - pub fn calculate_per_lp_delta( - &self, - delta: &PositionDelta, - fee_to_market: i128, - liquidity_split: AMMLiquiditySplit, - base_unit: i128, - ) -> DriftResult<(i128, i128, i128)> { - let total_lp_shares = if liquidity_split == AMMLiquiditySplit::LPOwned { - self.user_lp_shares - } else { - self.sqrt_k - }; - - // update Market per lp position - let per_lp_delta_base = get_proportion_i128( - delta.base_asset_amount.cast()?, - base_unit.cast()?, - total_lp_shares, //.safe_div_ceil(rebase_divisor.cast()?)?, - )?; - - let mut per_lp_delta_quote = get_proportion_i128( - delta.quote_asset_amount.cast()?, - base_unit.cast()?, - total_lp_shares, //.safe_div_ceil(rebase_divisor.cast()?)?, - )?; - - // user position delta is short => lp position delta is long - if per_lp_delta_base < 0 { - // add one => lp subtract 1 - per_lp_delta_quote = per_lp_delta_quote.safe_add(1)?; - } - - // 1/5 of fee auto goes to market - // the rest goes to lps/market proportional - let per_lp_fee: i128 = if fee_to_market > 0 { - get_proportion_i128( - fee_to_market, - LP_FEE_SLICE_NUMERATOR, - LP_FEE_SLICE_DENOMINATOR, - )? - .safe_mul(base_unit)? - .safe_div(total_lp_shares.cast::()?)? - } else { - 0 - }; - - Ok((per_lp_delta_base, per_lp_delta_quote, per_lp_fee)) - } - - pub fn get_target_base_asset_amount_per_lp(&self) -> DriftResult { - if self.target_base_asset_amount_per_lp == 0 { - return Ok(0_i128); - } - - let target_base_asset_amount_per_lp: i128 = if self.per_lp_base > 0 { - let rebase_divisor = 10_i128.pow(self.per_lp_base.abs().cast()?); - self.target_base_asset_amount_per_lp - .cast::()? - .safe_mul(rebase_divisor)? - } else if self.per_lp_base < 0 { - let rebase_divisor = 10_i128.pow(self.per_lp_base.abs().cast()?); - self.target_base_asset_amount_per_lp - .cast::()? - .safe_div(rebase_divisor)? - } else { - self.target_base_asset_amount_per_lp.cast::()? - }; - - Ok(target_base_asset_amount_per_lp) - } - - pub fn imbalanced_base_asset_amount_with_lp(&self) -> DriftResult { - let target_lp_gap = self - .base_asset_amount_per_lp - .safe_sub(self.get_target_base_asset_amount_per_lp()?)?; - - let base_unit = self.get_per_lp_base_unit()?.cast()?; - - get_proportion_i128(target_lp_gap, self.user_lp_shares, base_unit) - } - pub fn amm_wants_to_jit_make(&self, taker_direction: PositionDirection) -> DriftResult { let amm_wants_to_jit_make = match taker_direction { PositionDirection::Long => { @@ -1330,25 +1220,6 @@ impl AMM { Ok(amm_wants_to_jit_make && self.amm_jit_is_active()) } - pub fn amm_lp_wants_to_jit_make( - &self, - taker_direction: PositionDirection, - ) -> DriftResult { - if self.user_lp_shares == 0 { - return Ok(false); - } - - let amm_lp_wants_to_jit_make = match taker_direction { - PositionDirection::Long => { - self.base_asset_amount_per_lp > self.get_target_base_asset_amount_per_lp()? - } - PositionDirection::Short => { - self.base_asset_amount_per_lp < self.get_target_base_asset_amount_per_lp()? - } - }; - Ok(amm_lp_wants_to_jit_make && self.amm_lp_jit_is_active()) - } - pub fn amm_lp_allowed_to_jit_make(&self, amm_wants_to_jit_make: bool) -> DriftResult { // only allow lps to make when the amm inventory is below a certain level of available liquidity // i.e. 10% @@ -1360,12 +1231,7 @@ impl AMM { self.max_base_asset_reserve, )?; - let min_side_liquidity = max_bids.min(max_asks.abs()); - let protocol_owned_min_side_liquidity = get_proportion_i128( - min_side_liquidity, - self.sqrt_k.safe_sub(self.user_lp_shares)?, - self.sqrt_k, - )?; + let protocol_owned_min_side_liquidity = max_bids.min(max_asks.abs()); Ok(self.base_asset_amount_with_amm.abs() < protocol_owned_min_side_liquidity.safe_div(10)?) @@ -1378,10 +1244,6 @@ impl AMM { self.amm_jit_intensity > 0 } - pub fn amm_lp_jit_is_active(&self) -> bool { - self.amm_jit_intensity > 100 - } - pub fn reserve_price(&self) -> DriftResult { amm::calculate_price( self.quote_asset_reserve, @@ -1448,7 +1310,7 @@ impl AMM { .base_asset_amount_with_amm .unsigned_abs() .max(min_order_size_u128) - < self.sqrt_k.safe_sub(self.user_lp_shares)?) + < self.sqrt_k) && (min_order_size_u128 < max_bids.unsigned_abs().max(max_asks.unsigned_abs())); Ok(can_lower) diff --git a/programs/drift/src/validation/perp_market.rs b/programs/drift/src/validation/perp_market.rs index f562dd3f5f..3d7fb73e14 100644 --- a/programs/drift/src/validation/perp_market.rs +++ b/programs/drift/src/validation/perp_market.rs @@ -32,19 +32,16 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { )?; validate!( (market.amm.base_asset_amount_long + market.amm.base_asset_amount_short) - == market.amm.base_asset_amount_with_amm - + market.amm.base_asset_amount_with_unsettled_lp, + == market.amm.base_asset_amount_with_amm, ErrorCode::InvalidAmmDetected, "Market NET_BAA Error: market.amm.base_asset_amount_long={}, + market.amm.base_asset_amount_short={} != - market.amm.base_asset_amount_with_amm={} - + market.amm.base_asset_amount_with_unsettled_lp={}", + market.amm.base_asset_amount_with_amm={}", market.amm.base_asset_amount_long, market.amm.base_asset_amount_short, market.amm.base_asset_amount_with_amm, - market.amm.base_asset_amount_with_unsettled_lp, )?; validate!( @@ -84,15 +81,6 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.amm.quote_asset_reserve )?; - validate!( - market.amm.sqrt_k >= market.amm.user_lp_shares, - ErrorCode::InvalidAmmDetected, - "market {} market.amm.sqrt_k < market.amm.user_lp_shares: {} < {}", - market.market_index, - market.amm.sqrt_k, - market.amm.user_lp_shares, - )?; - let invariant_sqrt_u192 = crate::bn::U192::from(market.amm.sqrt_k); let invariant = invariant_sqrt_u192.safe_mul(invariant_sqrt_u192)?; let quote_asset_reserve = invariant @@ -229,22 +217,6 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.insurance_claim.revenue_withdraw_since_last_settle.unsigned_abs() )?; - validate!( - market.amm.base_asset_amount_per_lp < MAX_BASE_ASSET_AMOUNT_WITH_AMM as i128, - ErrorCode::InvalidAmmDetected, - "{} market.amm.base_asset_amount_per_lp too large: {}", - market.market_index, - market.amm.base_asset_amount_per_lp - )?; - - validate!( - market.amm.quote_asset_amount_per_lp < MAX_BASE_ASSET_AMOUNT_WITH_AMM as i128, - ErrorCode::InvalidAmmDetected, - "{} market.amm.quote_asset_amount_per_lp too large: {}", - market.market_index, - market.amm.quote_asset_amount_per_lp - )?; - Ok(()) } From 25ab531dafc0de16ba83d6810238baad7cbec2ea Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 16:56:55 -0400 Subject: [PATCH 004/159] make tests build --- programs/drift/src/controller/orders/tests.rs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 2f752066b8..eac2be1cba 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -486,7 +486,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -610,7 +609,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -734,7 +732,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -858,7 +855,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -981,7 +977,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1071,7 +1066,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1162,7 +1156,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1253,7 +1246,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1344,7 +1336,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1455,7 +1446,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1571,7 +1561,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1692,7 +1681,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1814,7 +1802,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1960,7 +1947,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -2081,7 +2067,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -2212,7 +2197,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2364,7 +2348,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2514,7 +2497,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2665,7 +2647,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2797,7 +2778,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -2928,7 +2908,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); From eba3f1123fb89bcee51677e90646a6ff2fbbdfe1 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 17:10:16 -0400 Subject: [PATCH 005/159] start sdk changes --- sdk/src/math/bankruptcy.ts | 3 +- sdk/src/user.ts | 358 +----- test-scripts/run-anchor-tests.sh | 4 - tests/liquidityProvider.ts | 1912 ------------------------------ tests/perpLpJit.ts | 1250 ------------------- tests/perpLpRiskMitigation.ts | 537 --------- tests/tradingLP.ts | 281 ----- 7 files changed, 25 insertions(+), 4320 deletions(-) delete mode 100644 tests/liquidityProvider.ts delete mode 100644 tests/perpLpJit.ts delete mode 100644 tests/perpLpRiskMitigation.ts delete mode 100644 tests/tradingLP.ts diff --git a/sdk/src/math/bankruptcy.ts b/sdk/src/math/bankruptcy.ts index 23054ccdaa..92172d59f4 100644 --- a/sdk/src/math/bankruptcy.ts +++ b/sdk/src/math/bankruptcy.ts @@ -19,8 +19,7 @@ export function isUserBankrupt(user: User): boolean { if ( !position.baseAssetAmount.eq(ZERO) || position.quoteAssetAmount.gt(ZERO) || - hasOpenOrders(position) || - position.lpShares.gt(ZERO) + hasOpenOrders(position) ) { return false; } diff --git a/sdk/src/user.ts b/sdk/src/user.ts index c57245ade6..59e9e96c43 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -211,6 +211,14 @@ export class User { return this.getPerpPositionForUserAccount(userAccount, marketIndex); } + public getPerpPositionOrEmpty(marketIndex: number): PerpPosition { + const userAccount = this.getUserAccount(); + return ( + this.getPerpPositionForUserAccount(userAccount, marketIndex) ?? + this.getEmptyPosition(marketIndex) + ); + } + public getPerpPositionAndSlot( marketIndex: number ): DataAndSlot { @@ -414,243 +422,12 @@ export class User { public getPerpBidAsks(marketIndex: number): [BN, BN] { const position = this.getPerpPosition(marketIndex); - const [lpOpenBids, lpOpenAsks] = this.getLPBidAsks(marketIndex); - - const totalOpenBids = lpOpenBids.add(position.openBids); - const totalOpenAsks = lpOpenAsks.add(position.openAsks); + const totalOpenBids = position.openBids; + const totalOpenAsks = position.openAsks; return [totalOpenBids, totalOpenAsks]; } - /** - * calculates the open bids and asks for an lp - * optionally pass in lpShares to see what bid/asks a user *would* take on - * @returns : lp open bids - * @returns : lp open asks - */ - public getLPBidAsks(marketIndex: number, lpShares?: BN): [BN, BN] { - const position = this.getPerpPosition(marketIndex); - - const lpSharesToCalc = lpShares ?? position?.lpShares; - - if (!lpSharesToCalc || lpSharesToCalc.eq(ZERO)) { - return [ZERO, ZERO]; - } - - const market = this.driftClient.getPerpMarketAccount(marketIndex); - const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk( - market.amm.baseAssetReserve, - market.amm.minBaseAssetReserve, - market.amm.maxBaseAssetReserve, - market.amm.orderStepSize - ); - - const lpOpenBids = marketOpenBids.mul(lpSharesToCalc).div(market.amm.sqrtK); - const lpOpenAsks = marketOpenAsks.mul(lpSharesToCalc).div(market.amm.sqrtK); - - return [lpOpenBids, lpOpenAsks]; - } - - /** - * calculates the market position if the lp position was settled - * @returns : the settled userPosition - * @returns : the dust base asset amount (ie, < stepsize) - * @returns : pnl from settle - */ - public getPerpPositionWithLPSettle( - marketIndex: number, - originalPosition?: PerpPosition, - burnLpShares = false, - includeRemainderInBaseAmount = false - ): [PerpPosition, BN, BN] { - originalPosition = - originalPosition ?? - this.getPerpPosition(marketIndex) ?? - this.getEmptyPosition(marketIndex); - - if (originalPosition.lpShares.eq(ZERO)) { - return [originalPosition, ZERO, ZERO]; - } - - const position = this.getClonedPosition(originalPosition); - const market = this.driftClient.getPerpMarketAccount(position.marketIndex); - - if (market.amm.perLpBase != position.perLpBase) { - // perLpBase = 1 => per 10 LP shares, perLpBase = -1 => per 0.1 LP shares - const expoDiff = market.amm.perLpBase - position.perLpBase; - const marketPerLpRebaseScalar = new BN(10 ** Math.abs(expoDiff)); - - if (expoDiff > 0) { - position.lastBaseAssetAmountPerLp = - position.lastBaseAssetAmountPerLp.mul(marketPerLpRebaseScalar); - position.lastQuoteAssetAmountPerLp = - position.lastQuoteAssetAmountPerLp.mul(marketPerLpRebaseScalar); - } else { - position.lastBaseAssetAmountPerLp = - position.lastBaseAssetAmountPerLp.div(marketPerLpRebaseScalar); - position.lastQuoteAssetAmountPerLp = - position.lastQuoteAssetAmountPerLp.div(marketPerLpRebaseScalar); - } - - position.perLpBase = position.perLpBase + expoDiff; - } - - const nShares = position.lpShares; - - // incorp unsettled funding on pre settled position - const quoteFundingPnl = calculateUnsettledFundingPnl(market, position); - - let baseUnit = AMM_RESERVE_PRECISION; - if (market.amm.perLpBase == position.perLpBase) { - if ( - position.perLpBase >= 0 && - position.perLpBase <= AMM_RESERVE_PRECISION_EXP.toNumber() - ) { - const marketPerLpRebase = new BN(10 ** market.amm.perLpBase); - baseUnit = baseUnit.mul(marketPerLpRebase); - } else if ( - position.perLpBase < 0 && - position.perLpBase >= -AMM_RESERVE_PRECISION_EXP.toNumber() - ) { - const marketPerLpRebase = new BN(10 ** Math.abs(market.amm.perLpBase)); - baseUnit = baseUnit.div(marketPerLpRebase); - } else { - throw 'cannot calc'; - } - } else { - throw 'market.amm.perLpBase != position.perLpBase'; - } - - const deltaBaa = market.amm.baseAssetAmountPerLp - .sub(position.lastBaseAssetAmountPerLp) - .mul(nShares) - .div(baseUnit); - const deltaQaa = market.amm.quoteAssetAmountPerLp - .sub(position.lastQuoteAssetAmountPerLp) - .mul(nShares) - .div(baseUnit); - - function sign(v: BN) { - return v.isNeg() ? new BN(-1) : new BN(1); - } - - function standardize(amount: BN, stepSize: BN) { - const remainder = amount.abs().mod(stepSize).mul(sign(amount)); - const standardizedAmount = amount.sub(remainder); - return [standardizedAmount, remainder]; - } - - const [standardizedBaa, remainderBaa] = standardize( - deltaBaa, - market.amm.orderStepSize - ); - - position.remainderBaseAssetAmount += remainderBaa.toNumber(); - - if ( - Math.abs(position.remainderBaseAssetAmount) > - market.amm.orderStepSize.toNumber() - ) { - const [newStandardizedBaa, newRemainderBaa] = standardize( - new BN(position.remainderBaseAssetAmount), - market.amm.orderStepSize - ); - position.baseAssetAmount = - position.baseAssetAmount.add(newStandardizedBaa); - position.remainderBaseAssetAmount = newRemainderBaa.toNumber(); - } - - let dustBaseAssetValue = ZERO; - if (burnLpShares && position.remainderBaseAssetAmount != 0) { - const oraclePriceData = this.driftClient.getOracleDataForPerpMarket( - position.marketIndex - ); - dustBaseAssetValue = new BN(Math.abs(position.remainderBaseAssetAmount)) - .mul(oraclePriceData.price) - .div(AMM_RESERVE_PRECISION) - .add(ONE); - } - - let updateType; - if (position.baseAssetAmount.eq(ZERO)) { - updateType = 'open'; - } else if (sign(position.baseAssetAmount).eq(sign(deltaBaa))) { - updateType = 'increase'; - } else if (position.baseAssetAmount.abs().gt(deltaBaa.abs())) { - updateType = 'reduce'; - } else if (position.baseAssetAmount.abs().eq(deltaBaa.abs())) { - updateType = 'close'; - } else { - updateType = 'flip'; - } - - let newQuoteEntry; - let pnl; - if (updateType == 'open' || updateType == 'increase') { - newQuoteEntry = position.quoteEntryAmount.add(deltaQaa); - pnl = ZERO; - } else if (updateType == 'reduce' || updateType == 'close') { - newQuoteEntry = position.quoteEntryAmount.sub( - position.quoteEntryAmount - .mul(deltaBaa.abs()) - .div(position.baseAssetAmount.abs()) - ); - pnl = position.quoteEntryAmount.sub(newQuoteEntry).add(deltaQaa); - } else { - newQuoteEntry = deltaQaa.sub( - deltaQaa.mul(position.baseAssetAmount.abs()).div(deltaBaa.abs()) - ); - pnl = position.quoteEntryAmount.add(deltaQaa.sub(newQuoteEntry)); - } - position.quoteEntryAmount = newQuoteEntry; - position.baseAssetAmount = position.baseAssetAmount.add(standardizedBaa); - position.quoteAssetAmount = position.quoteAssetAmount - .add(deltaQaa) - .add(quoteFundingPnl) - .sub(dustBaseAssetValue); - position.quoteBreakEvenAmount = position.quoteBreakEvenAmount - .add(deltaQaa) - .add(quoteFundingPnl) - .sub(dustBaseAssetValue); - - // update open bids/asks - const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk( - market.amm.baseAssetReserve, - market.amm.minBaseAssetReserve, - market.amm.maxBaseAssetReserve, - market.amm.orderStepSize - ); - const lpOpenBids = marketOpenBids - .mul(position.lpShares) - .div(market.amm.sqrtK); - const lpOpenAsks = marketOpenAsks - .mul(position.lpShares) - .div(market.amm.sqrtK); - position.openBids = lpOpenBids.add(position.openBids); - position.openAsks = lpOpenAsks.add(position.openAsks); - - // eliminate counting funding on settled position - if (position.baseAssetAmount.gt(ZERO)) { - position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong; - } else if (position.baseAssetAmount.lt(ZERO)) { - position.lastCumulativeFundingRate = - market.amm.cumulativeFundingRateShort; - } else { - position.lastCumulativeFundingRate = ZERO; - } - - const remainderBeforeRemoval = new BN(position.remainderBaseAssetAmount); - - if (includeRemainderInBaseAmount) { - position.baseAssetAmount = position.baseAssetAmount.add( - remainderBeforeRemoval - ); - position.remainderBaseAssetAmount = 0; - } - - return [position, remainderBeforeRemoval, pnl]; - } - /** * calculates Buying Power = free collateral / initial margin ratio * @returns : Precision QUOTE_PRECISION @@ -660,11 +437,7 @@ export class User { collateralBuffer = ZERO, enterHighLeverageMode = false ): BN { - const perpPosition = this.getPerpPositionWithLPSettle( - marketIndex, - undefined, - true - )[0]; + const perpPosition = this.getPerpPositionOrEmpty(marketIndex); const perpMarket = this.driftClient.getPerpMarketAccount(marketIndex); const oraclePriceData = this.getOracleDataForPerpMarket(marketIndex); @@ -777,8 +550,7 @@ export class User { (pos) => !pos.baseAssetAmount.eq(ZERO) || !pos.quoteAssetAmount.eq(ZERO) || - !(pos.openOrders == 0) || - !pos.lpShares.eq(ZERO) + !(pos.openOrders == 0) ); } @@ -850,14 +622,6 @@ export class User { market.quoteSpotMarketIndex ); - if (perpPosition.lpShares.gt(ZERO)) { - perpPosition = this.getPerpPositionWithLPSettle( - perpPosition.marketIndex, - undefined, - !!withWeightMarginCategory - )[0]; - } - let positionUnrealizedPnl = calculatePositionPNL( market, perpPosition, @@ -1434,15 +1198,6 @@ export class User { perpPosition.marketIndex ); - if (perpPosition.lpShares.gt(ZERO)) { - // is an lp, clone so we dont mutate the position - perpPosition = this.getPerpPositionWithLPSettle( - market.marketIndex, - this.getClonedPosition(perpPosition), - !!marginCategory - )[0]; - } - let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex ).price; @@ -1517,19 +1272,6 @@ export class User { liabilityValue = liabilityValue.add( new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) ); - - if (perpPosition.lpShares.gt(ZERO)) { - liabilityValue = liabilityValue.add( - BN.max( - QUOTE_PRECISION, - valuationPrice - .mul(market.amm.orderStepSize) - .mul(QUOTE_PRECISION) - .div(AMM_RESERVE_PRECISION) - .div(PRICE_PRECISION) - ) - ); - } } } @@ -1593,13 +1335,7 @@ export class User { oraclePriceData: OraclePriceData, includeOpenOrders = false ): BN { - const userPosition = - this.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - )[0] || this.getEmptyPosition(marketIndex); + const userPosition = this.getPerpPositionOrEmpty(marketIndex); const market = this.driftClient.getPerpMarketAccount( userPosition.marketIndex ); @@ -1620,13 +1356,7 @@ export class User { oraclePriceData: OraclePriceData, includeOpenOrders = false ): BN { - const userPosition = - this.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - )[0] || this.getEmptyPosition(marketIndex); + const userPosition = this.getPerpPositionOrEmpty(marketIndex); const market = this.driftClient.getPerpMarketAccount( userPosition.marketIndex ); @@ -2173,11 +1903,9 @@ export class User { const oraclePrice = this.driftClient.getOracleDataForSpotMarket(marketIndex).price; if (perpMarketWithSameOracle) { - const perpPosition = this.getPerpPositionWithLPSettle( - perpMarketWithSameOracle.marketIndex, - undefined, - true - )[0]; + const perpPosition = this.getPerpPositionOrEmpty( + perpMarketWithSameOracle.marketIndex + ); if (perpPosition) { let freeCollateralDeltaForPerp = this.calculateFreeCollateralDeltaForPerp( @@ -2263,9 +1991,7 @@ export class User { this.driftClient.getOracleDataForPerpMarket(marketIndex).price; const market = this.driftClient.getPerpMarketAccount(marketIndex); - const currentPerpPosition = - this.getPerpPositionWithLPSettle(marketIndex, undefined, true)[0] || - this.getEmptyPosition(marketIndex); + const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); positionBaseSizeChange = standardizeBaseAssetAmount( positionBaseSizeChange, @@ -2556,12 +2282,7 @@ export class User { closeQuoteAmount: BN, estimatedEntryPrice: BN = ZERO ): BN { - const currentPosition = - this.getPerpPositionWithLPSettle( - positionMarketIndex, - undefined, - true - )[0] || this.getEmptyPosition(positionMarketIndex); + const currentPosition = this.getPerpPositionOrEmpty(positionMarketIndex); const closeBaseAmount = currentPosition.baseAssetAmount .mul(closeQuoteAmount) @@ -2627,9 +2348,7 @@ export class User { ): { tradeSize: BN; oppositeSideTradeSize: BN } { let tradeSize = ZERO; let oppositeSideTradeSize = ZERO; - const currentPosition = - this.getPerpPositionWithLPSettle(targetMarketIndex, undefined, true)[0] || - this.getEmptyPosition(targetMarketIndex); + const currentPosition = this.getPerpPositionOrEmpty(targetMarketIndex); const targetSide = isVariant(tradeSide, 'short') ? 'short' : 'long'; @@ -3402,9 +3121,7 @@ export class User { return newLeverage; } - const currentPosition = - this.getPerpPositionWithLPSettle(targetMarketIndex)[0] || - this.getEmptyPosition(targetMarketIndex); + const currentPosition = this.getPerpPositionOrEmpty(targetMarketIndex); const perpMarket = this.driftClient.getPerpMarketAccount(targetMarketIndex); const oracleData = this.getOracleDataForPerpMarket(targetMarketIndex); @@ -3799,10 +3516,6 @@ export class User { oraclePriceData?: OraclePriceData; quoteOraclePriceData?: OraclePriceData; }): HealthComponent { - const settledLpPosition = this.getPerpPositionWithLPSettle( - perpPosition.marketIndex, - perpPosition - )[0]; const perpMarket = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); @@ -3814,7 +3527,7 @@ export class User { worstCaseBaseAssetAmount: worstCaseBaseAmount, worstCaseLiabilityValue, } = calculateWorstCasePerpLiabilityValue( - settledLpPosition, + perpPosition, perpMarket, oraclePrice ); @@ -3843,19 +3556,6 @@ export class User { new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) ); - if (perpPosition.lpShares.gt(ZERO)) { - marginRequirement = marginRequirement.add( - BN.max( - QUOTE_PRECISION, - oraclePrice - .mul(perpMarket.amm.orderStepSize) - .mul(QUOTE_PRECISION) - .div(AMM_RESERVE_PRECISION) - .div(PRICE_PRECISION) - ) - ); - } - return { marketIndex: perpMarket.marketIndex, size: worstCaseBaseAmount, @@ -3903,14 +3603,9 @@ export class User { perpMarket.quoteSpotMarketIndex ); - const settledPerpPosition = this.getPerpPositionWithLPSettle( - perpPosition.marketIndex, - perpPosition - )[0]; - const positionUnrealizedPnl = calculatePositionPNL( perpMarket, - settledPerpPosition, + perpPosition, true, oraclePriceData ); @@ -4062,12 +3757,7 @@ export class User { liquidationBuffer?: BN, includeOpenOrders?: boolean ): BN { - const currentPerpPosition = - this.getPerpPositionWithLPSettle( - marketToIgnore, - undefined, - !!marginCategory - )[0] || this.getEmptyPosition(marketToIgnore); + const currentPerpPosition = this.getPerpPositionOrEmpty(marketToIgnore); const oracleData = this.getOracleDataForPerpMarket(marketToIgnore); diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 2b10bd1bf7..a0e7494a33 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -44,7 +44,6 @@ test_files=( liquidatePerpPnlForDeposit.ts liquidateSpot.ts liquidateSpotSocialLoss.ts - liquidityProvider.ts marketOrder.ts marketOrderBaseAssetAmount.ts maxDeposit.ts @@ -60,8 +59,6 @@ test_files=( ordersWithSpread.ts pauseExchange.ts pauseDepositWithdraw.ts - perpLpJit.ts - perpLpRiskMitigation.ts phoenixTest.ts placeAndMakePerp.ts placeAndMakeSignedMsgBankrun.ts @@ -85,7 +82,6 @@ test_files=( surgePricing.ts switchboardTxCus.ts switchOracle.ts - tradingLP.ts triggerOrders.ts triggerSpotOrder.ts transferPerpPosition.ts diff --git a/tests/liquidityProvider.ts b/tests/liquidityProvider.ts deleted file mode 100644 index 4bfc4df65d..0000000000 --- a/tests/liquidityProvider.ts +++ /dev/null @@ -1,1912 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import { assert } from 'chai'; - -import { Program } from '@coral-xyz/anchor'; - -import * as web3 from '@solana/web3.js'; - -import { - TestClient, - QUOTE_PRECISION, - AMM_RESERVE_PRECISION, - EventSubscriber, - PRICE_PRECISION, - PositionDirection, - ZERO, - BN, - calculateAmmReservesAfterSwap, - calculatePrice, - User, - OracleSource, - SwapDirection, - Wallet, - isVariant, - LPRecord, - BASE_PRECISION, - getLimitOrderParams, - OracleGuardRails, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, - setFeedPriceNoProgram, - sleep, -} from './testHelpers'; -import { PerpPosition } from '../sdk'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -async function adjustOraclePostSwap(baa, swapDirection, market, context) { - const price = calculatePrice( - market.amm.baseAssetReserve, - market.amm.quoteAssetReserve, - market.amm.pegMultiplier - ); - - const [newQaa, newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - baa.abs(), - swapDirection - ); - - const newPrice = calculatePrice(newBaa, newQaa, market.amm.pegMultiplier); - const _newPrice = newPrice.toNumber() / PRICE_PRECISION.toNumber(); - await setFeedPriceNoProgram(context, _newPrice, market.amm.oracle); - - console.log('price => new price', price.toString(), newPrice.toString()); - - return _newPrice; -} - -async function createNewUser( - program, - provider, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -): Promise<[TestClient, User]> { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await provider.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - provider, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: provider.connection, - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: bulkAccountLoader - ? { - type: 'polling', - accountLoader: bulkAccountLoader, - } - : { - type: 'websocket', - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -async function fullClosePosition(driftClient, userPosition) { - console.log('=> closing:', userPosition.baseAssetAmount.toString()); - let position = (await driftClient.getUserAccount()).perpPositions[0]; - let sig; - let flag = true; - while (flag) { - sig = await driftClient.closePosition(0); - await driftClient.fetchAccounts(); - position = (await driftClient.getUserAccount()).perpPositions[0]; - if (position.baseAssetAmount.eq(ZERO)) { - flag = false; - } - } - - return sig; -} - -describe('liquidity providing', () => { - const chProgram = anchor.workspace.Drift as Program; - - let bankrunContextWrapper: BankrunContextWrapper; - - let bulkAccountLoader: TestBulkAccountLoader; - - async function _viewLogs(txsig) { - const tx = await bankrunContextWrapper.connection.getTransaction(txsig, { - commitment: 'confirmed', - }); - console.log('tx logs', tx.meta.logMessages); - } - async function delay(time) { - await new Promise((resolve) => setTimeout(resolve, time)); - } - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(300).mul(BASE_PRECISION); - const ammInitialQuoteAssetReserve = new BN(300).mul(BASE_PRECISION); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - const stableAmmInitialBaseAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - - const usdcAmount = new BN(1_000_000_000 * 1e6); - - let driftClient: TestClient; - let eventSubscriber: EventSubscriber; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let poorDriftClient: TestClient; - let poorDriftClientUser: User; - - let solusdc; - let solusdc2; - - before(async () => { - const context = await startAnchor('', [], []); - - bankrunContextWrapper = new BankrunContextWrapper(context); - - bulkAccountLoader = new TestBulkAccountLoader( - bankrunContextWrapper.connection, - 'processed', - 1 - ); - - usdcMint = await mockUSDCMint(bankrunContextWrapper); - - eventSubscriber = new EventSubscriber( - bankrunContextWrapper.connection, - chProgram - ); - await eventSubscriber.subscribe(); - - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - ]; - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // await driftClient.updateMarketBaseAssetAmountStepSize( - // new BN(0), - // new BN(1) - // ); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - [poorDriftClient, poorDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - QUOTE_PRECISION, - oracleInfos, - undefined, - bulkAccountLoader - ); - }); - - after(async () => { - await eventSubscriber.unsubscribe(); - - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - - await poorDriftClient.unsubscribe(); - await poorDriftClientUser.unsubscribe(); - }); - - const lpCooldown = 1; - - it('burn with standardized baa', async () => { - console.log('adding liquidity...'); - const initMarginReq = driftClientUser.getInitialMarginRequirement(); - assert(initMarginReq.eq(ZERO)); - - let market = driftClient.getPerpMarketAccount(0); - const lpAmount = new BN(100 * BASE_PRECISION.toNumber()); // 100 / (100 + 300) = 1/4 - const _sig = await driftClient.addPerpLpShares( - lpAmount, - market.marketIndex - ); - - await driftClient.fetchAccounts(); - - const addLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(addLiquidityRecord.action, 'addLiquidity')); - assert(addLiquidityRecord.nShares.eq(lpAmount)); - assert(addLiquidityRecord.marketIndex === 0); - assert( - addLiquidityRecord.user.equals( - await driftClient.getUserAccountPublicKey() - ) - ); - - const [bids, asks] = driftClientUser.getLPBidAsks(0); - console.log( - 'bar, min_bar, max_bar:', - market.amm.baseAssetReserve.toString(), - market.amm.minBaseAssetReserve.toString(), - market.amm.maxBaseAssetReserve.toString() - ); - console.log('LP open bids/asks:', bids.toString(), asks.toString()); - assert(bids.eq(new BN(41419999989))); - assert(asks.eq(new BN(-29288643749))); - - await driftClient.placePerpOrder( - getLimitOrderParams({ - baseAssetAmount: BASE_PRECISION, - marketIndex: 0, - direction: PositionDirection.LONG, // ++ bids - price: PRICE_PRECISION, - }) - ); - await driftClient.placePerpOrder( - getLimitOrderParams({ - baseAssetAmount: BASE_PRECISION, - marketIndex: 0, - direction: PositionDirection.SHORT, // ++ asks - price: PRICE_PRECISION.mul(new BN(100)), - }) - ); - - await driftClient.fetchAccounts(); - const [bids2, asks2] = driftClientUser.getPerpBidAsks(0); - assert(bids2.eq(bids.add(BASE_PRECISION))); - assert(asks2.eq(asks.sub(BASE_PRECISION))); - - await driftClient.cancelOrders(); - - await driftClient.fetchAccounts(); - const position3 = driftClientUser.getPerpPosition(0); - assert(position3.openOrders == 0); - assert(position3.openAsks.eq(ZERO)); - assert(position3.openBids.eq(ZERO)); - - const newInitMarginReq = driftClientUser.getInitialMarginRequirement(); - console.log(initMarginReq.toString(), '->', newInitMarginReq.toString()); - assert(newInitMarginReq.eq(new BN(9284008))); // 8284008 + $1 - - // ensure margin calcs didnt modify user position - const _position = driftClientUser.getPerpPosition(0); - assert(_position.openAsks.eq(ZERO)); - assert(_position.openBids.eq(ZERO)); - - const stepSize = new BN(1 * BASE_PRECISION.toNumber()); - await driftClient.updatePerpMarketStepSizeAndTickSize( - 0, - stepSize, - driftClient.getPerpMarketAccount(0).amm.orderTickSize - ); - - let user = await driftClientUser.getUserAccount(); - console.log('lpUser lpShares:', user.perpPositions[0].lpShares.toString()); - console.log( - 'lpUser baa:', - user.perpPositions[0].baseAssetAmount.toString() - ); - - assert(user.perpPositions[0].lpShares.eq(new BN('100000000000'))); - assert(user.perpPositions[0].baseAssetAmount.eq(ZERO)); - // some user goes long (lp should get a short) - console.log('user trading...'); - - market = driftClient.getPerpMarketAccount(0); - assert(market.amm.sqrtK.eq(new BN('400000000000'))); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - - const [newQaa, _newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - tradeSize.abs(), - SwapDirection.ADD - ); - const quoteAmount = newQaa.sub(market.amm.quoteAssetReserve); - const lpQuoteAmount = quoteAmount.mul(lpAmount).div(market.amm.sqrtK); - console.log( - lpQuoteAmount.mul(QUOTE_PRECISION).div(AMM_RESERVE_PRECISION).toString() - ); - - const newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - const sig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex, - new BN((newPrice * PRICE_PRECISION.toNumber() * 99) / 100) - ); - await _viewLogs(sig); - - // amm gets 33 (3/4 * 50 = 37.5) - // lp gets stepSize (1/4 * 50 = 12.5 => 10 with remainder 2.5) - // 2.5 / 12.5 = 0.2 - - await traderDriftClient.fetchAccounts(); - const traderUserAccount = await traderDriftClient.getUserAccount(); - const position = traderUserAccount.perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - - assert(position.baseAssetAmount.eq(new BN('-5000000000'))); - - await driftClient.fetchAccounts(); - const marketNetBaa = - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm; - - console.log('removing liquidity...'); - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - await _viewLogs(_txSig); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(settleLiquidityRecord.action, 'settleLiquidity')); - assert(settleLiquidityRecord.marketIndex === 0); - assert( - settleLiquidityRecord.user.equals( - await driftClient.getUserAccountPublicKey() - ) - ); - - // net baa doesnt change on settle - await driftClient.fetchAccounts(); - assert( - driftClient - .getPerpMarketAccount(0) - .amm.baseAssetAmountWithAmm.eq(marketNetBaa) - ); - - const marketAfter = driftClient.getPerpMarketAccount(0); - assert( - marketAfter.amm.baseAssetAmountWithUnsettledLp.eq(new BN('-250000000')) - ); - assert(marketAfter.amm.baseAssetAmountWithAmm.eq(new BN('-3750000000'))); - - user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - - assert( - settleLiquidityRecord.deltaBaseAssetAmount.eq(lpPosition.baseAssetAmount) - ); - assert( - settleLiquidityRecord.deltaQuoteAssetAmount.eq( - lpPosition.quoteAssetAmount - ) - ); - - console.log( - 'lp tokens, baa, qaa:', - lpPosition.lpShares.toString(), - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString(), - // lpPosition.unsettledPnl.toString(), - lpPosition.lastBaseAssetAmountPerLp.toString(), - lpPosition.lastQuoteAssetAmountPerLp.toString() - ); - - // assert(lpPosition.lpShares.eq(new BN(0))); - await driftClient.fetchAccounts(); - assert(user.perpPositions[0].baseAssetAmount.eq(new BN(1000000000))); // lp is long - console.log( - '=> net baa:', - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.toString() - ); - assert(user.perpPositions[0].quoteAssetAmount.eq(new BN(-1233700))); - // assert(user.perpPositions[0].unsettledPnl.eq(new BN(900))); - // remainder goes into the last - assert(user.perpPositions[0].lastBaseAssetAmountPerLp.eq(new BN(12500000))); - assert(user.perpPositions[0].lastQuoteAssetAmountPerLp.eq(new BN(-12337))); - - market = await driftClient.getPerpMarketAccount(0); - console.log( - market.amm.quoteAssetAmountPerLp.toString(), - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN(12500000))); - assert(market.amm.quoteAssetAmountPerLp.eq(new BN(-12337))); - console.log(user.perpPositions[0].remainderBaseAssetAmount.toString()); // lp remainder - assert(user.perpPositions[0].remainderBaseAssetAmount != 0); // lp remainder - assert(user.perpPositions[0].remainderBaseAssetAmount == 250000000); // lp remainder - - // remove - console.log('removing liquidity...'); - await driftClient.removePerpLpShares(0); - - await driftClient.fetchAccounts(); - - const removeLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(removeLiquidityRecord.action, 'removeLiquidity')); - assert(removeLiquidityRecord.nShares.eq(lpAmount)); - assert(removeLiquidityRecord.marketIndex === 0); - assert( - removeLiquidityRecord.user.equals( - await driftClient.getUserAccountPublicKey() - ) - ); - console.log( - 'removeLiquidityRecord.deltaQuoteAssetAmount', - removeLiquidityRecord.deltaQuoteAssetAmount.toString() - ); - assert(removeLiquidityRecord.deltaBaseAssetAmount.eq(ZERO)); - assert(removeLiquidityRecord.deltaQuoteAssetAmount.eq(new BN('-243866'))); // show pnl from burn in record - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - await fullClosePosition( - traderDriftClient, - traderDriftClient.getUserAccount().perpPositions[0] - ); - const traderUserAccount2 = - traderDriftClient.getUserAccount().perpPositions[0]; - - console.log( - traderUserAccount2.lpShares.toString(), - traderUserAccount2.baseAssetAmount.toString(), - traderUserAccount2.quoteAssetAmount.toString() - ); - - console.log('closing lp ...'); - console.log( - user.perpPositions[0].baseAssetAmount - .div(new BN(BASE_PRECISION.toNumber())) - .toString() - ); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - - const _ttxsig = await fullClosePosition(driftClient, user.perpPositions[0]); - // await _viewLogs(ttxsig); - - await driftClient.updatePerpMarketStepSizeAndTickSize( - 0, - new BN(1), - market.amm.orderTickSize - ); - - const user2 = await driftClientUser.getUserAccount(); - const position2 = user2.perpPositions[0]; - console.log( - position2.lpShares.toString(), - position2.baseAssetAmount.toString(), - position2.quoteAssetAmount.toString() - ); - - await driftClient.fetchAccounts(); - console.log( - '=> net baa:', - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.toString() - ); - assert( - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.eq(ZERO) - ); - - console.log('done!'); - }); - - it('settles lp', async () => { - console.log('adding liquidity...'); - - const market = driftClient.getPerpMarketAccount(0); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - - let user = await driftClientUser.getUserAccount(); - console.log(user.perpPositions[0].lpShares.toString()); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - const [settledLPPosition, _, sdkPnl] = - driftClientUser.getPerpPositionWithLPSettle(0); - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await await driftClientUser.getUserAccount(); - const position = user.perpPositions[0]; - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - console.log( - 'deltaBaseAssetAmount:', - settleLiquidityRecord.deltaBaseAssetAmount.toString() - ); - console.log( - 'deltaQuoteAssetAmount:', - settleLiquidityRecord.deltaQuoteAssetAmount.toString() - ); - - assert(settleLiquidityRecord.pnl.toString() === sdkPnl.toString()); - - // gets a short on settle - console.log( - 'simulated settle position:', - settledLPPosition.baseAssetAmount.toString(), - settledLPPosition.quoteAssetAmount.toString(), - settledLPPosition.quoteEntryAmount.toString() - ); - - // gets a short on settle - console.log( - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString(), - position.quoteEntryAmount.toString(), - position.remainderBaseAssetAmount.toString() - ); - - assert(settledLPPosition.baseAssetAmount.eq(position.baseAssetAmount)); - assert(settledLPPosition.quoteAssetAmount.eq(position.quoteAssetAmount)); - assert(settledLPPosition.quoteEntryAmount.eq(position.quoteEntryAmount)); - assert( - settledLPPosition.remainderBaseAssetAmount === - position.remainderBaseAssetAmount - ); - - console.log( - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - assert(position.baseAssetAmount.lt(ZERO)); - assert(position.quoteAssetAmount.gt(ZERO)); - assert(position.lpShares.gt(ZERO)); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await _viewLogs(_txSig); - - user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - const lpTokenAmount = lpPosition.lpShares; - assert(lpTokenAmount.eq(ZERO)); - - console.log( - 'lp position:', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString() - ); - - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await fullClosePosition( - traderDriftClient, - trader.perpPositions[0] - ); - await _viewLogs(_txsig); - - const traderPosition = (await traderDriftClient.getUserAccount()) - .perpPositions[0]; - console.log( - 'trader position:', - traderPosition.baseAssetAmount.toString(), - traderPosition.quoteAssetAmount.toString() - ); - - console.log('closing lp ...'); - const market2 = driftClient.getPerpMarketAccount(0); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.ADD, - market2, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, user.perpPositions[0]); - - await driftClient.fetchAccounts(); - console.log( - '=> net baa:', - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.toString() - ); - assert( - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.eq(ZERO) - ); - - console.log('done!'); - }); - - it('provides and removes liquidity', async () => { - let market = driftClient.getPerpMarketAccount(0); - const prevSqrtK = market.amm.sqrtK; - const prevbar = market.amm.baseAssetReserve; - const prevqar = market.amm.quoteAssetReserve; - const prevQaa = - driftClient.getUserAccount().perpPositions[0].quoteAssetAmount; - - console.log('adding liquidity...'); - try { - const _txsig = await driftClient.addPerpLpShares( - new BN(100 * AMM_RESERVE_PRECISION.toNumber()), - market.marketIndex - ); - } catch (e) { - console.error(e); - } - await delay(lpCooldown + 1000); - - market = driftClient.getPerpMarketAccount(0); - console.log( - 'sqrtK:', - prevSqrtK.toString(), - '->', - market.amm.sqrtK.toString() - ); - console.log( - 'baseAssetReserve:', - prevbar.toString(), - '->', - market.amm.baseAssetReserve.toString() - ); - console.log( - 'quoteAssetReserve:', - prevqar.toString(), - '->', - market.amm.quoteAssetReserve.toString() - ); - - // k increases = more liquidity - assert(prevSqrtK.lt(market.amm.sqrtK)); - assert(prevqar.lt(market.amm.quoteAssetReserve)); - assert(prevbar.lt(market.amm.baseAssetReserve)); - - const lpShares = (await driftClientUser.getUserAccount()).perpPositions[0] - .lpShares; - console.log('lpShares:', lpShares.toString()); - assert(lpShares.gt(ZERO)); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - const user = await driftClientUser.getUserAccount(); - const lpTokenAmount = user.perpPositions[0].lpShares; - console.log('lp token amount:', lpTokenAmount.toString()); - assert(lpTokenAmount.eq(ZERO)); - // dont round down for no change - assert(user.perpPositions[0].quoteAssetAmount.eq(prevQaa)); - - console.log('asset reserves:'); - console.log(prevSqrtK.toString(), market.amm.sqrtK.toString()); - console.log(prevbar.toString(), market.amm.baseAssetReserve.toString()); - console.log(prevqar.toString(), market.amm.quoteAssetReserve.toString()); - - const errThreshold = new BN(500); - assert(prevSqrtK.eq(market.amm.sqrtK)); - assert( - prevbar.sub(market.amm.baseAssetReserve).abs().lte(errThreshold), - prevbar.sub(market.amm.baseAssetReserve).abs().toString() - ); - assert( - prevqar.sub(market.amm.quoteAssetReserve).abs().lte(errThreshold), - prevqar.sub(market.amm.quoteAssetReserve).abs().toString() - ); - assert(prevSqrtK.eq(market.amm.sqrtK)); - }); - - it('mints too many lp tokens', async () => { - console.log('adding liquidity...'); - const market = driftClient.getPerpMarketAccount(0); - try { - const _sig = await poorDriftClient.addPerpLpShares( - market.amm.sqrtK.mul(new BN(5)), - market.marketIndex - ); - _viewLogs(_sig); - assert(false); - } catch (e) { - console.error(e.message); - assert(e.message.includes('0x1773')); // insufficient collateral - } - }); - - it('provides lp, users shorts, removes lp, lp has long', async () => { - await driftClient.fetchAccounts(); - await traderDriftClient.fetchAccounts(); - console.log('adding liquidity...'); - - const traderUserAccount3 = await driftClient.getUserAccount(); - const position3 = traderUserAccount3.perpPositions[0]; - console.log( - 'lp position:', - position3.baseAssetAmount.toString(), - position3.quoteAssetAmount.toString() - ); - - const traderUserAccount0 = await traderDriftClient.getUserAccount(); - const position0 = traderUserAccount0.perpPositions[0]; - console.log( - 'trader position:', - position0.baseAssetAmount.toString(), - position0.quoteAssetAmount.toString() - ); - assert(position0.baseAssetAmount.eq(new BN('0'))); - - const market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.netBaseAssetAmount:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('0'))); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - // await delay(lpCooldown + 1000); - - let user = await driftClientUser.getUserAccount(); - console.log('lpUser lpShares:', user.perpPositions[0].lpShares.toString()); - console.log( - 'lpUser baa:', - user.perpPositions[0].baseAssetAmount.toString() - ); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(40 * BASE_PRECISION.toNumber()); - const _newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - try { - const _txsig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - // new BN(newPrice * PRICE_PRECISION.toNumber()) - ); - } catch (e) { - console.error(e); - } - - await traderDriftClient.fetchAccounts(); - const market1 = driftClient.getPerpMarketAccount(0); - console.log( - 'market1.amm.netBaseAssetAmount:', - market1.amm.baseAssetAmountWithAmm.toString() - ); - const ammLpRatio = - market1.amm.userLpShares.toNumber() / market1.amm.sqrtK.toNumber(); - - console.log('amm ratio:', ammLpRatio, '(', 40 * ammLpRatio, ')'); - - assert(market1.amm.baseAssetAmountWithAmm.eq(new BN('-30000000000'))); - - const traderUserAccount = await traderDriftClient.getUserAccount(); - // console.log(traderUserAccount); - const position = traderUserAccount.perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await _viewLogs(_txSig); - - user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - const lpTokenAmount = lpPosition.lpShares; - - console.log( - 'lp tokens', - lpTokenAmount.toString(), - 'baa, qaa', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString() - // lpPosition.unsettledPnl.toString() - ); - - const removeLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(removeLiquidityRecord.action, 'removeLiquidity')); - assert( - removeLiquidityRecord.deltaBaseAssetAmount.eq( - lpPosition.baseAssetAmount.sub(position3.baseAssetAmount) - ) - ); - assert( - removeLiquidityRecord.deltaQuoteAssetAmount.eq( - lpPosition.quoteAssetAmount.sub(position3.quoteAssetAmount) - ) - ); - - assert(lpTokenAmount.eq(new BN(0))); - console.log(user.perpPositions[0].baseAssetAmount.toString()); - console.log(user.perpPositions[0].quoteAssetAmount.toString()); - assert(user.perpPositions[0].baseAssetAmount.eq(new BN('10000000000'))); // lp is long - assert(user.perpPositions[0].quoteAssetAmount.eq(new BN(-9550985))); - - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - await fullClosePosition( - traderDriftClient, - traderUserAccount.perpPositions[0] - ); - - console.log('closing lp ...'); - console.log( - user.perpPositions[0].baseAssetAmount - .div(new BN(BASE_PRECISION.toNumber())) - .toString() - ); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, user.perpPositions[0]); - - const user2 = await driftClientUser.getUserAccount(); - const position2 = user2.perpPositions[0]; - console.log( - position2.lpShares.toString(), - position2.baseAssetAmount.toString(), - position2.quoteAssetAmount.toString() - ); - - console.log('done!'); - }); - - it('provides lp, users longs, removes lp, lp has short', async () => { - const market = driftClient.getPerpMarketAccount(0); - - console.log('adding liquidity...'); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - // await delay(lpCooldown + 1000); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(40 * BASE_PRECISION.toNumber()); - const _newPrice0 = await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(newPrice0 * PRICE_PRECISION.toNumber()) - ); - - const position = (await traderDriftClient.getUserAccount()) - .perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await _viewLogs(_txSig); - - await driftClientUser.fetchAccounts(); - const user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - const lpTokenAmount = lpPosition.lpShares; - - console.log('lp tokens', lpTokenAmount.toString()); - console.log( - 'baa, qaa, qea', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString(), - lpPosition.quoteEntryAmount.toString() - - // lpPosition.unsettledPnl.toString() - ); - - assert(lpTokenAmount.eq(ZERO)); - assert(user.perpPositions[0].baseAssetAmount.eq(new BN('-10000000000'))); // lp is short - assert(user.perpPositions[0].quoteAssetAmount.eq(new BN('11940540'))); - assert(user.perpPositions[0].quoteEntryAmount.eq(new BN('11139500'))); - - console.log('closing trader...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await fullClosePosition(traderDriftClient, position); - - console.log('closing lp ...'); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, lpPosition); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const user2 = await driftClientUser.getUserAccount(); - const lpPosition2 = user2.perpPositions[0]; - - console.log('lp tokens', lpPosition2.lpShares.toString()); - console.log( - 'lp position for market', - lpPosition2.marketIndex, - ':\n', - 'baa, qaa, qea', - lpPosition2.baseAssetAmount.toString(), - lpPosition2.quoteAssetAmount.toString(), - lpPosition2.quoteEntryAmount.toString() - ); - assert(lpPosition2.baseAssetAmount.eq(ZERO)); - - console.log('done!'); - }); - - it('lp burns a partial position', async () => { - const market = driftClient.getPerpMarketAccount(0); - - console.log('adding liquidity...'); - await driftClient.addPerpLpShares( - new BN(100).mul(AMM_RESERVE_PRECISION), - market.marketIndex - ); - // await delay(lpCooldown + 1000); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const user0 = await driftClient.getUserAccount(); - const position0 = user0.perpPositions[0]; - console.log( - 'assert LP has 0 position in market index', - market.marketIndex, - ':', - position0.baseAssetAmount.toString(), - position0.quoteAssetAmount.toString() - ); - console.log(position0.lpShares.toString()); - - const baa0 = position0.baseAssetAmount; - assert(baa0.eq(ZERO)); - - console.log('user trading...'); - const tradeSize = new BN(40 * BASE_PRECISION.toNumber()); - const _newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - // new BN(newPrice * PRICE_PRECISION.toNumber()) - ); - - console.log('removing liquidity...'); - let user = await driftClient.getUserAccount(); - let position = user.perpPositions[0]; - - const fullShares = position.lpShares; - const halfShares = position.lpShares.div(new BN(2)); - const otherHalfShares = fullShares.sub(halfShares); - - try { - const _txSig = await driftClient.removePerpLpShares( - market.marketIndex, - halfShares - ); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - user = await driftClient.getUserAccount(); - position = user.perpPositions[0]; - console.log( - 'lp first half burn:', - user.perpPositions[0].baseAssetAmount.toString(), - user.perpPositions[0].quoteAssetAmount.toString(), - user.perpPositions[0].lpShares.toString() - ); - - const baa = user.perpPositions[0].baseAssetAmount; - const qaa = user.perpPositions[0].quoteAssetAmount; - assert(baa.eq(new BN(10000000000))); - assert(qaa.eq(new BN(-6860662))); - - console.log('removing the other half of liquidity'); - await driftClient.removePerpLpShares(market.marketIndex, otherHalfShares); - - await driftClient.fetchAccounts(); - - user = await driftClient.getUserAccount(); - console.log( - 'lp second half burn:', - user.perpPositions[0].baseAssetAmount.toString(), - user.perpPositions[0].quoteAssetAmount.toString(), - user.perpPositions[0].lpShares.toString() - ); - // lp is already settled so full burn baa is already in baa - assert(user.perpPositions[0].lpShares.eq(ZERO)); - - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - // await traderDriftClient.closePosition(new BN(0)); - const trader = await traderDriftClient.getUserAccount(); - const _txsig = await fullClosePosition( - traderDriftClient, - trader.perpPositions[0] - ); - - console.log('closing lp ...'); - await adjustOraclePostSwap( - baa, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, user.perpPositions[0]); - }); - - it('settles lp with pnl', async () => { - console.log('adding liquidity...'); - - const market = driftClient.getPerpMarketAccount(0); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - - let user = await driftClientUser.getUserAccount(); - console.log(user.perpPositions[0].lpShares.toString()); - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - // some user goes long (lp should get a short + pnl for closing long on settle) - console.log('user trading...'); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await await driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - }); - - it('update per lp base (0->1)', async () => { - //ensure non-zero for test - - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp(0, 169); - - await driftClient.fetchAccounts(); - const marketBefore = driftClient.getPerpMarketAccount(0); - console.log( - 'marketBefore.amm.totalFeeEarnedPerLp', - marketBefore.amm.totalFeeEarnedPerLp.toString() - ); - assert(marketBefore.amm.totalFeeEarnedPerLp.eq(new BN('272'))); - - const txSig1 = await driftClient.updatePerpMarketPerLpBase(0, 1); - await _viewLogs(txSig1); - - await sleep(1400); // todo? - await driftClient.fetchAccounts(); - const marketAfter = driftClient.getPerpMarketAccount(0); - - assert( - marketAfter.amm.totalFeeEarnedPerLp.eq( - marketBefore.amm.totalFeeEarnedPerLp.mul(new BN(10)) - ) - ); - - assert( - marketAfter.amm.baseAssetAmountPerLp.eq( - marketBefore.amm.baseAssetAmountPerLp.mul(new BN(10)) - ) - ); - - assert( - marketAfter.amm.quoteAssetAmountPerLp.eq( - marketBefore.amm.quoteAssetAmountPerLp.mul(new BN(10)) - ) - ); - console.log(marketAfter.amm.targetBaseAssetAmountPerLp); - console.log(marketBefore.amm.targetBaseAssetAmountPerLp); - - assert( - marketAfter.amm.targetBaseAssetAmountPerLp == - marketBefore.amm.targetBaseAssetAmountPerLp - ); - assert(marketAfter.amm.totalFeeEarnedPerLp.eq(new BN('2720'))); - - assert(marketBefore.amm.perLpBase == 0); - console.log('marketAfter.amm.perLpBase:', marketAfter.amm.perLpBase); - assert(marketAfter.amm.perLpBase == 1); - }); - - it('settle lp position after perLpBase change', async () => { - // some user goes long (lp should get a short + pnl for closing long on settle) - - const market = driftClient.getPerpMarketAccount(0); - console.log( - 'baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(ZERO)); - // await delay(lpCooldown + 1000); - - const user = await driftClientUser.getUserAccount(); - console.log(user.perpPositions[0].lpShares.toString()); - console.log(user.perpPositions[0].perLpBase); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - - console.log('user trading...'); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - const marketAfter0 = driftClient.getPerpMarketAccount(0); - - console.log( - 'baseAssetAmountWithUnsettledLp:', - marketAfter0.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert( - marketAfter0.amm.baseAssetAmountWithUnsettledLp.eq(new BN('1250000000')) - ); - - const netValueBefore = await driftClient.getUser().getNetSpotMarketValue(); - const posBefore0: PerpPosition = await driftClient - .getUser() - .getPerpPosition(0); - assert(posBefore0.perLpBase == 0); - - const posBefore: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - // console.log(posBefore); - assert(posBefore.perLpBase == 1); // properly sets it - - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - await _viewLogs(_txSig); - await driftClient.fetchAccounts(); - const marketAfter1 = driftClient.getPerpMarketAccount(0); - - console.log( - 'baseAssetAmountWithUnsettledLp:', - marketAfter1.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert(marketAfter1.amm.baseAssetAmountWithUnsettledLp.eq(new BN('0'))); - - const posAfter0: PerpPosition = await driftClient - .getUser() - .getPerpPosition(0); - assert(posAfter0.perLpBase == 1); - - const posAfter: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - - assert(posAfter.perLpBase == 1); - assert( - posAfter0.lastBaseAssetAmountPerLp.gt(posBefore0.lastBaseAssetAmountPerLp) - ); - // console.log(posAfter.lastBaseAssetAmountPerLp.toString()); - // console.log(posBefore.lastBaseAssetAmountPerLp.toString()); - - assert(posAfter.lastBaseAssetAmountPerLp.eq(new BN('625000000'))); - assert(posBefore.lastBaseAssetAmountPerLp.eq(new BN('750000000'))); - - const netValueAfter = await driftClient.getUser().getNetSpotMarketValue(); - - assert(netValueBefore.eq(netValueAfter)); - - const marketAfter2 = driftClient.getPerpMarketAccount(0); - - console.log( - 'baseAssetAmountWithUnsettledLp:', - marketAfter2.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert(marketAfter2.amm.baseAssetAmountWithUnsettledLp.eq(new BN('0'))); - console.log( - 'marketBefore.amm.totalFeeEarnedPerLp', - marketAfter2.amm.totalFeeEarnedPerLp.toString() - ); - assert(marketAfter2.amm.totalFeeEarnedPerLp.eq(new BN('2826'))); - }); - - it('add back lp shares from 0, after rebase', async () => { - const leShares = driftClientUser.getPerpPosition(0).lpShares; - await driftClient.removePerpLpShares(0, leShares); - await driftClient.fetchAccounts(); - - await driftClient.updatePerpMarketPerLpBase(0, 2); // update from 1->2 - - const posBeforeReadd: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log(posBeforeReadd.baseAssetAmount.toString()); - console.log(posBeforeReadd.quoteAssetAmount.toString()); - console.log(posBeforeReadd.lastBaseAssetAmountPerLp.toString()); - console.log(posBeforeReadd.lastQuoteAssetAmountPerLp.toString()); - console.log( - posBeforeReadd.lpShares.toString(), - posBeforeReadd.perLpBase.toString() - ); - - await driftClient.addPerpLpShares(leShares, 0); // lmao why is this different param order - - const posBefore: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posBefore'); - console.log(posBefore.baseAssetAmount.toString()); - console.log(posBefore.quoteAssetAmount.toString()); - console.log(posBefore.lastBaseAssetAmountPerLp.toString()); - console.log(posBefore.lastQuoteAssetAmountPerLp.toString()); - console.log(posBefore.lpShares.toString(), posBefore.perLpBase.toString()); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - const market = driftClient.getPerpMarketAccount(0); - - console.log('user trading...'); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfter: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfter'); - console.log(posAfter.baseAssetAmount.toString()); - console.log(posAfter.quoteAssetAmount.toString()); - console.log(posAfter.lastBaseAssetAmountPerLp.toString()); - console.log(posAfter.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfter.perLpBase.toString()); - - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfterSettle: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfterSettle'); - console.log(posAfterSettle.baseAssetAmount.toString()); - console.log(posAfterSettle.quoteAssetAmount.toString()); - console.log(posAfterSettle.lastBaseAssetAmountPerLp.toString()); - console.log(posAfterSettle.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfterSettle.perLpBase.toString()); - - assert(posAfterSettle.baseAssetAmount.eq(posAfter.baseAssetAmount)); - assert(posAfterSettle.quoteAssetAmount.eq(posAfter.quoteAssetAmount)); - }); - - it('settled at negative rebase value', async () => { - await driftClient.updatePerpMarketPerLpBase(0, 1); - await driftClient.updatePerpMarketPerLpBase(0, 0); - await driftClient.updatePerpMarketPerLpBase(0, -1); - await driftClient.updatePerpMarketPerLpBase(0, -2); - await driftClient.updatePerpMarketPerLpBase(0, -3); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - const market = driftClient.getPerpMarketAccount(0); - - console.log('user trading...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - ); - await _viewLogs(_txsig); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfter: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfter'); - console.log(posAfter.baseAssetAmount.toString()); - console.log(posAfter.quoteAssetAmount.toString()); - console.log(posAfter.lastBaseAssetAmountPerLp.toString()); - console.log(posAfter.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfter.perLpBase.toString()); - - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfterSettle: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfterSettle'); - console.log(posAfterSettle.baseAssetAmount.toString()); - console.log(posAfterSettle.quoteAssetAmount.toString()); - console.log(posAfterSettle.lastBaseAssetAmountPerLp.toString()); - console.log(posAfterSettle.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfterSettle.perLpBase.toString()); - - assert(posAfterSettle.baseAssetAmount.eq(posAfter.baseAssetAmount)); - assert(posAfterSettle.quoteAssetAmount.eq(posAfter.quoteAssetAmount)); - }); - - it('permissionless lp burn', async () => { - const lpAmount = new BN(1 * BASE_PRECISION.toNumber()); - const _sig = await driftClient.addPerpLpShares(lpAmount, 0); - - const time = bankrunContextWrapper.connection.getTime(); - const _2sig = await driftClient.updatePerpMarketExpiry(0, new BN(time + 5)); - - await sleep(5000); - - await driftClient.fetchAccounts(); - const market = driftClient.getPerpMarketAccount(0); - console.log(market.status); - - await traderDriftClient.removePerpLpSharesInExpiringMarket( - 0, - await driftClient.getUserAccountPublicKey() - ); - - await driftClientUser.fetchAccounts(); - const position = driftClientUser.getPerpPosition(0); - console.log(position); - // assert(position.lpShares.eq(ZERO)); - }); - - return; - - it('lp gets paid in funding (todo)', async () => { - const market = driftClient.getPerpMarketAccount(1); - const marketIndex = market.marketIndex; - - console.log('adding liquidity to market ', marketIndex, '...'); - try { - const _sig = await driftClient.addPerpLpShares( - new BN(100_000).mul(new BN(BASE_PRECISION.toNumber())), - marketIndex - ); - } catch (e) { - console.error(e); - } - await delay(lpCooldown + 1000); - - console.log('user trading...'); - // const trader0 = await traderDriftClient.getUserAccount(); - const tradeSize = new BN(100).mul(AMM_RESERVE_PRECISION); - - const newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - console.log('market', marketIndex, 'post trade price:', newPrice); - try { - const _txig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - marketIndex, - new BN(newPrice * PRICE_PRECISION.toNumber()) - ); - } catch (e) { - console.error(e); - } - - console.log('updating funding rates'); - const _txsig = await driftClient.updateFundingRate(marketIndex, solusdc2); - - console.log('removing liquidity...'); - try { - const _txSig = await driftClient.removePerpLpShares(marketIndex); - _viewLogs(_txSig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - - const user = driftClientUser.getUserAccount(); - // const feePayment = new BN(1300000); - // const fundingPayment = new BN(900000); - - // dont get paid in fees bc the sqrtk is so big that fees dont get given to the lps - // TODO - // assert(user.perpPositions[1].unsettledPnl.eq(fundingPayment.add(feePayment))); - const position1 = user.perpPositions[1]; - console.log( - 'lp position:', - position1.baseAssetAmount.toString(), - position1.quoteAssetAmount.toString(), - 'vs step size:', - market.amm.orderStepSize.toString() - ); - assert(user.perpPositions[1].baseAssetAmount.eq(ZERO)); // lp has no position - assert( - user.perpPositions[1].baseAssetAmount.abs().lt(market.amm.orderStepSize) - ); - // const trader = traderDriftClient.getUserAccount(); - // await adjustOraclePostSwap( - // trader.perpPositions[1].baseAssetAmount, - // SwapDirection.ADD, - // market - // ); - // await traderDriftClient.closePosition(market.marketIndex); // close lp position - - // console.log('closing lp ...'); - // console.log(user.perpPositions[1].baseAssetAmount.toString()); - // await adjustOraclePostSwap( - // user.perpPositions[1].baseAssetAmount, - // SwapDirection.REMOVE, - // market - // ); - }); - - // // TODO - // it('provides and removes liquidity too fast', async () => { - // const market = driftClient.getPerpMarketAccount(0); - - // const lpShares = new BN(100 * AMM_RESERVE_PRECISION); - // const addLpIx = await driftClient.getAddLiquidityIx( - // lpShares, - // market.marketIndex - // ); - // const removeLpIx = await driftClient.getRemoveLiquidityIx( - // market.marketIndex, - // lpShares - // ); - - // const tx = new web3.Transaction().add(addLpIx).add(removeLpIx); - // try { - // await provider.sendAll([{ tx }]); - // assert(false); - // } catch (e) { - // console.error(e); - // assert(e.message.includes('0x17ce')); - // } - // }); - - // it('removes liquidity when market position is small', async () => { - // console.log('adding liquidity...'); - // await driftClient.addLiquidity(usdcAmount, new BN(0)); - // - // console.log('user trading...'); - // await traderDriftClient.openPosition( - // PositionDirection.LONG, - // new BN(1 * 1e6), - // new BN(0) - // ); - // - // console.log('removing liquidity...'); - // await driftClient.removeLiquidity(new BN(0)); - // - // const user = driftClient.getUserAccount(); - // const position = user.perpPositions[0]; - // - // // small loss - // assert(position.unsettledPnl.lt(ZERO)); - // // no position - // assert(position.baseAssetAmount.eq(ZERO)); - // assert(position.quoteAssetAmount.eq(ZERO)); - // }); - // - // uncomment when settle fcn is ready - - /* it('adds additional liquidity to an already open lp', async () => { - console.log('adding liquidity...'); - const lp_amount = new BN(300 * 1e6); - const _txSig = await driftClient.addLiquidity(lp_amount, new BN(0)); - - console.log( - 'tx logs', - (await connection.getTransaction(txsig, { commitment: 'confirmed' })).meta - .logMessages - ); - - const init_user = driftClientUser.getUserAccount(); - await driftClient.addLiquidity(lp_amount, new BN(0)); - const user = driftClientUser.getUserAccount(); - - const init_tokens = init_user.perpPositions[0].lpTokens; - const tokens = user.perpPositions[0].lpTokens; - console.log(init_tokens.toString(), tokens.toString()); - assert(init_tokens.lt(tokens)); - - await driftClient.removeLiquidity(new BN(0)); - }); */ - - /* it('settles an lps position', async () => { - console.log('adding liquidity...'); - await driftClient.addLiquidity(usdcAmount, new BN(0)); - - let user = driftClient.getUserAccount(); - const baa = user.perpPositions[0].baseAssetAmount; - const qaa = user.perpPositions[0].quoteAssetAmount; - const upnl = user.perpPositions[0].unsettledPnl; - - console.log('user trading...'); - await traderDriftClient.openPosition( - PositionDirection.SHORT, - new BN(115 * 1e5), - new BN(0) - ); - - console.log('settling...'); - await traderDriftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - new BN(0) - ); - - user = driftClient.getUserAccount(); - const position = user.perpPositions[0]; - const post_baa = position.baseAssetAmount; - const post_qaa = position.quoteAssetAmount; - const post_upnl = position.unsettledPnl; - - // they got the market position + upnl - console.log(baa.toString(), post_baa.toString()); - console.log(qaa.toString(), post_qaa.toString()); - console.log(upnl.toString(), post_upnl.toString()); - assert(!post_baa.eq(baa)); - assert(post_qaa.gt(qaa)); - assert(!post_upnl.eq(upnl)); - - // other sht was updated - const market = driftClient.getPerpMarketAccount(new BN(0)); - assert(market.amm.netBaseAssetAmount.eq(position.lastNetBaseAssetAmount)); - assert( - market.amm.totalFeeMinusDistributions.eq( - position.lastTotalFeeMinusDistributions - ) - ); - - const _txSig = await driftClient.removeLiquidity(new BN(0)); - - console.log('done!'); - }); */ - - /* it('simulates a settle via sdk', async () => { - const userPosition2 = driftClient.getUserAccount().perpPositions[0]; - console.log( - userPosition2.baseAssetAmount.toString(), - userPosition2.quoteAssetAmount.toString(), - userPosition2.unsettledPnl.toString() - ); - - console.log('add lp ...'); - await driftClient.addLiquidity(usdcAmount, new BN(0)); - - console.log('user trading...'); - await traderDriftClient.openPosition( - PositionDirection.SHORT, - new BN(115 * 1e5), - new BN(0) - ); - - const [settledPosition, result, _] = driftClientUser.getPerpPositionWithLPSettle( - new BN(0) - ); - - console.log('settling...'); - const _txSig = await traderDriftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - new BN(0) - ); - console.log( - 'tx logs', - (await connection.getTransaction(txsig, { commitment: 'confirmed' })).meta - .logMessages - ); - const userPosition = driftClient.getUserAccount().perpPositions[0]; - - console.log( - userPosition.baseAssetAmount.toString(), - settledPosition.baseAssetAmount.toString(), - - userPosition.quoteAssetAmount.toString(), - settledPosition.quoteAssetAmount.toString(), - - userPosition.unsettledPnl.toString(), - settledPosition.unsettledPnl.toString() - ); - assert(result == SettleResult.RECIEVED_MARKET_POSITION); - assert(userPosition.baseAssetAmount.eq(settledPosition.baseAssetAmount)); - assert(userPosition.quoteAssetAmount.eq(settledPosition.quoteAssetAmount)); - assert(userPosition.unsettledPnl.eq(settledPosition.unsettledPnl)); - }); */ -}); diff --git a/tests/perpLpJit.ts b/tests/perpLpJit.ts deleted file mode 100644 index 12acebca50..0000000000 --- a/tests/perpLpJit.ts +++ /dev/null @@ -1,1250 +0,0 @@ -import * as web3 from '@solana/web3.js'; -import * as anchor from '@coral-xyz/anchor'; -import { Program } from '@coral-xyz/anchor'; -import { assert } from 'chai'; - -import { - TestClient, - QUOTE_PRECISION, - EventSubscriber, - PRICE_PRECISION, - PositionDirection, - ZERO, - BN, - calculateAmmReservesAfterSwap, - calculatePrice, - User, - OracleSource, - SwapDirection, - Wallet, - LPRecord, - BASE_PRECISION, - getLimitOrderParams, - OracleGuardRails, - PostOnlyParams, - isVariant, - calculateBidAskPrice, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, - setFeedPriceNoProgram, - sleep, - // sleep, -} from './testHelpers'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -let lastOrderRecordsLength = 0; - -async function adjustOraclePostSwap(baa, swapDirection, market, context) { - const price = calculatePrice( - market.amm.baseAssetReserve, - market.amm.quoteAssetReserve, - market.amm.pegMultiplier - ); - - const [newQaa, newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - baa.abs(), - swapDirection - ); - - const newPrice = calculatePrice(newBaa, newQaa, market.amm.pegMultiplier); - const _newPrice = newPrice.toNumber() / PRICE_PRECISION.toNumber(); - await setFeedPriceNoProgram(context, _newPrice, market.amm.oracle); - - console.log('price => new price', price.toString(), newPrice.toString()); - - return _newPrice; -} - -async function createNewUser( - program, - context, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -) { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await context.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - context, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: context.connection.toConnection(), - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1, 2, 3], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: bulkAccountLoader - ? { - type: 'polling', - accountLoader: bulkAccountLoader, - } - : { - type: 'websocket', - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -describe('lp jit', () => { - const chProgram = anchor.workspace.Drift as Program; - - async function _viewLogs(txsig) { - bankrunContextWrapper.printTxLogs(txsig); - } - async function delay(time) { - await new Promise((resolve) => setTimeout(resolve, time)); - } - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(300).mul(BASE_PRECISION); - const ammInitialQuoteAssetReserve = new BN(300).mul(BASE_PRECISION); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - const stableAmmInitialBaseAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - - const usdcAmount = new BN(1_000_000_000 * 1e6); // 1 milli - - let driftClient: TestClient; - let eventSubscriber: EventSubscriber; - - let bulkAccountLoader: TestBulkAccountLoader; - - let bankrunContextWrapper: BankrunContextWrapper; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let poorDriftClient: TestClient; - let poorDriftClientUser: User; - - let solusdc; - let solusdc2; - let solusdc3; - let btcusdc; - - 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); - - solusdc3 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - btcusdc = await mockOracleNoProgram(bankrunContextWrapper, 26069, -7); - - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - { publicKey: solusdc3, source: OracleSource.PYTH }, - { publicKey: btcusdc, source: OracleSource.PYTH }, - ]; - - // @ts-ignore - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // await driftClient.updateMarketBaseAssetAmountStepSize( - // new BN(0), - // new BN(1) - // ); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - // third market - await driftClient.initializePerpMarket( - 2, - solusdc3, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - - // third market - await driftClient.initializePerpMarket( - 3, - btcusdc, - stableAmmInitialBaseAssetReserve.div(new BN(1000)), - stableAmmInitialQuoteAssetReserve.div(new BN(1000)), - new BN(0), - new BN(26690 * 1000) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - // @ts-ignore - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - - // @ts-ignore - [poorDriftClient, poorDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - QUOTE_PRECISION.mul(new BN(10000)), - oracleInfos, - undefined, - bulkAccountLoader - ); - }); - - after(async () => { - await eventSubscriber.unsubscribe(); - - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - - await poorDriftClient.unsubscribe(); - await poorDriftClientUser.unsubscribe(); - }); - - const lpCooldown = 1; - it('perp jit check (amm jit intensity = 0)', async () => { - const marketIndex = 0; - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - 0, - BASE_PRECISION.toNumber() - ); - sleep(1200); - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString(), - 'target:', - market.amm.targetBaseAssetAmountPerLp - ); - assert(market.amm.sqrtK.eq(new BN('300000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - // assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('400000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '100000000000'); // 10 * 1e9 - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-12500000'))); - - // some user goes long (lp should get a short + pnl for closing long on settle) - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-25000000'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('7500000000'))); - - // add jit maker going other way - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(0.9 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(0.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(0.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - const order = traderDriftClientUser.getOrderByUserOrderId(1); - assert(!order.postOnly); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(1.011 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - const txSig = await poorDriftClient.placeAndMakePerpOrder( - makerOrderParams, - { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - } - ); - await _viewLogs(txSig); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-12500000'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('3750000000'))); - console.log( - 'market.amm.baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('1250000000'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await await driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - }); - it('perp jit check (amm jit intensity = 100)', async () => { - const marketIndex = 1; - await driftClient.updateAmmJitIntensity(marketIndex, 100); - - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - marketIndex, - BASE_PRECISION.toNumber() - ); - await delay(lpCooldown + 1000); - - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1100000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - await driftClientUser.fetchAccounts(); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '100000000000'); // 10 * 1e9 - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-4545454'))); - - // some user goes long (lp should get a short + pnl for closing long on settle) - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-9090908'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('9090909200'))); - - // add jit maker going other way - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(0.9 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(0.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(0.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - const order = traderDriftClient.getUser().getOrderByUserOrderId(1); - assert(!order.postOnly); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(1.011 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - const txSig = await poorDriftClient.placeAndMakePerpOrder( - makerOrderParams, - { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - } - ); - await _viewLogs(txSig); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-5455090'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('5204991000'))); - console.log( - 'market.amm.baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('545509000'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - await driftClientUser.fetchAccounts(); - user = await driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - // assert(settleLiquidityRecord.pnl.eq(sdkPnl)); //TODO - }); - it('perp jit check (amm jit intensity = 200)', async () => { - const marketIndex = 2; - - await driftClient.updateAmmJitIntensity(marketIndex, 200); - - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - marketIndex, - BASE_PRECISION.toNumber() - ); - sleep(1200); - - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert( - market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber(), - `targetBaseAssetAmountPerLp: ${ - market.amm.targetBaseAssetAmountPerLp - } != ${BASE_PRECISION.toNumber()}` - ); - - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1100000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - await driftClientUser.fetchAccounts(); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '100000000000'); // 10 * 1e9 - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-4545454'))); - - // some user goes long (lp should get a short + pnl for closing long on settle) - // try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - // } catch (e) { - // console.log(e); - // } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-9090908'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('9090909200'))); - - // const trader = await traderDriftClient.getUserAccount(); - // console.log( - // 'trader size', - // trader.perpPositions[0].baseAssetAmount.toString() - // ); - - for (let i = 0; i < 10; i++) { - // add jit maker going other way - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(0.9 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(0.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(0.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - // console.log(takerOrderParams); - const order = traderDriftClient.getUser().getOrderByUserOrderId(1); - // console.log(order); - - assert(!order.postOnly); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(1.011 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - // console.log('maker:', makerOrderParams); - - const txSig = await poorDriftClient.placeAndMakePerpOrder( - makerOrderParams, - { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - } - ); - await _viewLogs(txSig); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - console.log( - 'market.amm.baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - - if (i == 0) { - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-5227727'))); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('5227727300'))); - assert( - market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('522772700')) - ); - } - } - market = driftClient.getPerpMarketAccount(marketIndex); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('12499904'))); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('90400'))); - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('-1249990400'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await driftClientUser.getUserAccount(); - const orderRecords = eventSubscriber.getEventsArray('OrderActionRecord'); - - const matchOrderRecord = orderRecords[1]; - assert( - isVariant(matchOrderRecord.actionExplanation, 'orderFilledWithMatchJit') - ); - assert(matchOrderRecord.baseAssetAmountFilled.toString(), '3750000000'); - assert(matchOrderRecord.quoteAssetAmountFilled.toString(), '3791212'); - - const jitOrderRecord = orderRecords[2]; - assert(isVariant(jitOrderRecord.actionExplanation, 'orderFilledWithLpJit')); - assert(jitOrderRecord.baseAssetAmountFilled.toString(), '1250000000'); - assert(jitOrderRecord.quoteAssetAmountFilled.toString(), '1263738'); - - // console.log('len of orderRecords', orderRecords.length); - lastOrderRecordsLength = orderRecords.length; - - // Convert the array to a JSON string - // const fs = require('fs'); - // // Custom replacer function to convert BN values to numerical representation - // const replacer = (key, value) => { - // if (value instanceof BN) { - // return value.toString(10); // Convert BN to base-10 string - // } - // return value; - // }; - // const jsonOrderRecords = JSON.stringify(orderRecords, replacer); - - // // Write the JSON string to a file - // fs.writeFile('orderRecords.json', jsonOrderRecords, 'utf8', (err) => { - // if (err) { - // console.error('Error writing to JSON file:', err); - // return; - // } - // console.log('orderRecords successfully written to orderRecords.json'); - // }); - - // assert(orderRecords) - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - // assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - }); - it('perp jit check BTC inout (amm jit intensity = 200)', async () => { - const marketIndex = 3; - - await driftClient.updateAmmJitIntensity(marketIndex, 200); - await driftClient.updatePerpMarketCurveUpdateIntensity(marketIndex, 100); - await driftClient.updatePerpMarketMaxSpread(marketIndex, 100000); - await driftClient.updatePerpMarketBaseSpread(marketIndex, 10000); - sleep(1200); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == 0); - - console.log('adding liquidity...'); - const _sig = await driftClient.addPerpLpShares( - BASE_PRECISION, - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('2000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - let [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - let perpy = await driftClientUser.getPerpPosition(marketIndex); - - assert(perpy.lpShares.toString() == '1000000000'); // 1e9 - console.log( - 'user.perpPositions[0].baseAssetAmount:', - perpy.baseAssetAmount.toString() - ); - assert(perpy.baseAssetAmount.toString() == '0'); // no fills - - // trader goes long - const tradeSize = BASE_PRECISION.div(new BN(20)); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - perpy = await driftClientUser.getPerpPosition(marketIndex); - assert(perpy.baseAssetAmount.toString() == '0'); // unsettled - - await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - marketIndex - ); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - perpy = await driftClientUser.getPerpPosition(marketIndex); - console.log('perpy.baseAssetAmount:', perpy.baseAssetAmount.toString()); - assert(perpy.baseAssetAmount.toString() == '-10000000'); // settled - - [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(26000 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(26400.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(26000.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - // console.log(takerOrderParams); - // const order = traderDriftClientUser.getOrderByUserOrderId(1); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(26488.88 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - await poorDriftClient.placeAndMakePerpOrder(makerOrderParams, { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - }); - - await driftClient.fetchAccounts(); - const marketAfter = driftClient.getPerpMarketAccount(marketIndex); - const orderRecords = eventSubscriber.getEventsArray('OrderActionRecord'); - - console.log('len of orderRecords', orderRecords.length); - assert(orderRecords.length - lastOrderRecordsLength == 7); - lastOrderRecordsLength = orderRecords.length; - // Convert the array to a JSON string - - // console.log(marketAfter); - console.log(marketAfter.amm.baseAssetAmountPerLp.toString()); - console.log(marketAfter.amm.quoteAssetAmountPerLp.toString()); - console.log(marketAfter.amm.baseAssetAmountWithUnsettledLp.toString()); - console.log(marketAfter.amm.baseAssetAmountWithAmm.toString()); - - assert(marketAfter.amm.baseAssetAmountPerLp.eq(new BN(-5000000))); - assert(marketAfter.amm.quoteAssetAmountPerLp.eq(new BN(144606790 - 1))); - assert(marketAfter.amm.baseAssetAmountWithUnsettledLp.eq(new BN(-5000000))); - assert(marketAfter.amm.baseAssetAmountWithAmm.eq(new BN(5000000))); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPos = driftClientUser.getPerpPosition(marketIndex); - console.log(perpPos.baseAssetAmount.toString()); - assert(perpPos.baseAssetAmount.toString() == '-10000000'); - - const [settledPos, dustPos, lpPnl] = - driftClientUser.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - ); - // console.log('settlePos:', settledPos); - console.log('dustPos:', dustPos.toString()); - console.log('lpPnl:', lpPnl.toString()); - - assert(dustPos.toString() == '0'); - assert(lpPnl.toString() == '6134171'); - - const _sig2 = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - marketIndex - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPosAfter = driftClientUser.getPerpPosition(marketIndex); - console.log( - 'perpPosAfter.baseAssetAmount:', - perpPosAfter.baseAssetAmount.toString() - ); - assert(perpPosAfter.baseAssetAmount.toString() == '-5000000'); - assert(perpPosAfter.baseAssetAmount.eq(settledPos.baseAssetAmount)); - - const takerOrderParams2 = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize.mul(new BN(20)), - price: new BN(26000 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(26400.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(26000.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams2); - await traderDriftClient.fetchAccounts(); - // console.log(takerOrderParams); - // const order = traderDriftClientUser.getOrderByUserOrderId(1); - - const makerOrderParams2 = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize.mul(new BN(20)), - price: new BN(26488.88 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - await poorDriftClient.placeAndMakePerpOrder(makerOrderParams2, { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - }); - const marketAfter2 = driftClient.getPerpMarketAccount(marketIndex); - - console.log(marketAfter2.amm.baseAssetAmountPerLp.toString()); - console.log(marketAfter2.amm.quoteAssetAmountPerLp.toString()); - console.log(marketAfter2.amm.baseAssetAmountWithUnsettledLp.toString()); - console.log(marketAfter2.amm.baseAssetAmountWithAmm.toString()); - - assert(marketAfter2.amm.baseAssetAmountPerLp.eq(new BN(-2500000))); - assert(marketAfter2.amm.quoteAssetAmountPerLp.eq(new BN(78437566))); - assert( - marketAfter2.amm.baseAssetAmountWithUnsettledLp.eq(new BN(-2500000)) - ); - assert(marketAfter2.amm.baseAssetAmountWithAmm.eq(new BN(2500000))); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPos2 = driftClientUser.getPerpPosition(marketIndex); - console.log(perpPos2.baseAssetAmount.toString()); - assert(perpPos2.baseAssetAmount.toString() == '-5000000'); - - const [settledPos2, dustPos2, lpPnl2] = - driftClientUser.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - ); - // console.log('settlePos:', settledPos2); - console.log('dustPos:', dustPos2.toString()); - console.log('lpPnl:', lpPnl2.toString()); - - assert(dustPos2.toString() == '0'); - assert(lpPnl2.toString() == '3067086'); - - await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - marketIndex - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPosAfter2 = driftClientUser.getPerpPosition(marketIndex); - console.log( - 'perpPosAfter2.baseAssetAmount:', - perpPosAfter2.baseAssetAmount.toString() - ); - assert(perpPosAfter2.baseAssetAmount.toString() == '-2500000'); - assert(perpPosAfter2.baseAssetAmount.eq(settledPos2.baseAssetAmount)); - - const orderRecords2 = eventSubscriber.getEventsArray('OrderActionRecord'); - console.log('len of orderRecords', orderRecords2.length); - // assert(orderRecords.length - lastOrderRecordsLength == 7); - lastOrderRecordsLength = orderRecords2.length; - - // const fs = require('fs'); - // // Custom replacer function to convert BN values to numerical representation - // const replacer = (key, value) => { - // if (value instanceof BN) { - // return value.toString(10); // Convert BN to base-10 string - // } - // return value; - // }; - // const jsonOrderRecords2 = JSON.stringify(orderRecords2, replacer); - - // // Write the JSON string to a file - // fs.writeFile('orderRecords.json', jsonOrderRecords2, 'utf8', (err) => { - // if (err) { - // console.error('Error writing to JSON file:', err); - // return; - // } - // console.log('orderRecords successfully written to orderRecords.json'); - // }); - }); -}); diff --git a/tests/perpLpRiskMitigation.ts b/tests/perpLpRiskMitigation.ts deleted file mode 100644 index 12b8037f31..0000000000 --- a/tests/perpLpRiskMitigation.ts +++ /dev/null @@ -1,537 +0,0 @@ -import * as web3 from '@solana/web3.js'; -import * as anchor from '@coral-xyz/anchor'; -import { Program } from '@coral-xyz/anchor'; -import { assert } from 'chai'; - -import { - TestClient, - QUOTE_PRECISION, - EventSubscriber, - PRICE_PRECISION, - PositionDirection, - ZERO, - BN, - calculateAmmReservesAfterSwap, - calculatePrice, - User, - OracleSource, - SwapDirection, - Wallet, - LPRecord, - BASE_PRECISION, - OracleGuardRails, - isVariant, - MARGIN_PRECISION, - SettlePnlMode, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, - setFeedPriceNoProgram, - sleep, - // sleep, -} from './testHelpers'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -async function adjustOraclePostSwap(baa, swapDirection, market, context) { - const price = calculatePrice( - market.amm.baseAssetReserve, - market.amm.quoteAssetReserve, - market.amm.pegMultiplier - ); - - const [newQaa, newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - baa.abs(), - swapDirection - ); - - const newPrice = calculatePrice(newBaa, newQaa, market.amm.pegMultiplier); - const _newPrice = newPrice.toNumber() / PRICE_PRECISION.toNumber(); - await setFeedPriceNoProgram(context, _newPrice, market.amm.oracle); - - console.log('price => new price', price.toString(), newPrice.toString()); - - return _newPrice; -} - -async function createNewUser( - program, - context: BankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -): Promise<[TestClient, User]> { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await context.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - context, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: context.connection.toConnection(), - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1, 2, 3], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -describe('lp risk mitigation', () => { - const chProgram = anchor.workspace.Drift as Program; - - async function _viewLogs(txsig) { - bankrunContextWrapper.printTxLogs(txsig); - } - async function delay(time) { - await new Promise((resolve) => setTimeout(resolve, time)); - } - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(10000).mul(BASE_PRECISION); - const ammInitialQuoteAssetReserve = new BN(10000).mul(BASE_PRECISION); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - const stableAmmInitialBaseAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - - const usdcAmount = new BN(5000 * 1e6); // 2000 bucks - - let driftClient: TestClient; - let eventSubscriber: EventSubscriber; - - let bulkAccountLoader: TestBulkAccountLoader; - - let bankrunContextWrapper: BankrunContextWrapper; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let poorDriftClient: TestClient; - let poorDriftClientUser: User; - - let solusdc; - let solusdc2; - let solusdc3; - let btcusdc; - - 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); - - solusdc3 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - btcusdc = await mockOracleNoProgram(bankrunContextWrapper, 26069, -7); - - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - { publicKey: solusdc3, source: OracleSource.PYTH }, - { publicKey: btcusdc, source: OracleSource.PYTH }, - ]; - - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // await driftClient.updateMarketBaseAssetAmountStepSize( - // new BN(0), - // new BN(1) - // ); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - // third market - await driftClient.initializePerpMarket( - 2, - solusdc3, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - - // third market - await driftClient.initializePerpMarket( - 3, - btcusdc, - stableAmmInitialBaseAssetReserve.div(new BN(1000)), - stableAmmInitialQuoteAssetReserve.div(new BN(1000)), - new BN(0), - new BN(26690 * 1000) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - await traderDriftClient.updateUserAdvancedLp([ - { - advancedLp: true, - subAccountId: 0, - }, - ]); - [poorDriftClient, poorDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - QUOTE_PRECISION.mul(new BN(10000)), - oracleInfos, - undefined, - bulkAccountLoader - ); - await poorDriftClient.updateUserAdvancedLp([ - { - advancedLp: true, - subAccountId: 0, - }, - ]); - }); - - after(async () => { - await eventSubscriber.unsubscribe(); - - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - - await poorDriftClient.unsubscribe(); - await poorDriftClientUser.unsubscribe(); - }); - - const lpCooldown = 1; - it('perp risk mitigation', async () => { - const marketIndex = 0; - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - marketIndex, - BASE_PRECISION.toNumber() - ); - sleep(1200); - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString(), - 'target:', - market.amm.targetBaseAssetAmountPerLp - ); - assert(market.amm.sqrtK.eq(new BN('10000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - // assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - const _sig = await driftClient.addPerpLpShares( - new BN(1000 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('11000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '1000000000000'); // 1000 * 1e9 - - // lp goes short - const tradeSize = new BN(500 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex, - new BN(0.1 * PRICE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('45454545'))); - await driftClientUser.fetchAccounts(); - await driftClient.accountSubscriber.setSpotOracleMap(); - - console.log( - 'driftClientUser.getFreeCollateral()=', - driftClientUser.getFreeCollateral().toString() - ); - assert(driftClientUser.getFreeCollateral().eq(new BN('4761073360'))); - // some user goes long (lp should get more short) - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('0'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('0'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const [userPos, dustBase, sdkPnl] = - driftClientUser.getPerpPositionWithLPSettle(0); - - console.log('baseAssetAmount:', userPos.baseAssetAmount.toString()); - console.log('dustBase:', dustBase.toString()); - - console.log('settling...'); - try { - const _txsigg = await driftClient.settlePNL( - await driftClient.getUserAccountPublicKey(), - await driftClient.getUserAccount(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - - const perpLiqPrice = driftClientUser.liquidationPrice(0); - console.log('perpLiqPrice:', perpLiqPrice.toString()); - - await setFeedPriceNoProgram(bankrunContextWrapper, 8, solusdc); - console.log('settling...'); - try { - const _txsigg = await driftClient.settlePNL( - await driftClient.getUserAccountPublicKey(), - await driftClient.getUserAccount(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - - await driftClient.updateUserCustomMarginRatio([ - { - marginRatio: MARGIN_PRECISION.toNumber(), - subAccountId: 0, - }, - ]); - - await sleep(1000); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - console.log( - 'driftClientUser.getUserAccount().openOrders=', - driftClientUser.getUserAccount().openOrders - ); - assert(driftClientUser.getUserAccount().openOrders == 0); - - console.log('settling after margin ratio update...'); - try { - const _txsigg = await driftClient.settleMultiplePNLs( - await driftClient.getUserAccountPublicKey(), - await driftClient.getUserAccount(), - [0], - SettlePnlMode.TRY_SETTLE - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const afterReduceOrdersAccount = driftClientUser.getUserAccount(); - assert(afterReduceOrdersAccount.openOrders == 1); - - const leOrder = afterReduceOrdersAccount.orders[0]; - console.log(leOrder); - assert(leOrder.auctionDuration == 80); - assert(leOrder.auctionStartPrice.lt(leOrder.auctionEndPrice)); - assert(leOrder.auctionEndPrice.gt(ZERO)); - assert(leOrder.reduceOnly); - assert(!leOrder.postOnly); - assert(leOrder.marketIndex == 0); - assert(leOrder.baseAssetAmount.eq(new BN('500000000000'))); - assert(isVariant(leOrder.direction, 'long')); - assert(isVariant(leOrder.existingPositionDirection, 'short')); - - const afterReduceShares = - afterReduceOrdersAccount.perpPositions[0].lpShares; - - console.log('afterReduceShares=', afterReduceShares.toString()); - assert(afterReduceShares.lt(new BN(1000 * BASE_PRECISION.toNumber()))); - assert(afterReduceShares.eq(new BN('400000000000'))); - }); -}); diff --git a/tests/tradingLP.ts b/tests/tradingLP.ts deleted file mode 100644 index 565178183e..0000000000 --- a/tests/tradingLP.ts +++ /dev/null @@ -1,281 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import { assert } from 'chai'; -import { BN, User, OracleSource, Wallet, MARGIN_PRECISION } from '../sdk'; - -import { Program } from '@coral-xyz/anchor'; - -import * as web3 from '@solana/web3.js'; - -import { - TestClient, - PRICE_PRECISION, - PositionDirection, - ZERO, - OracleGuardRails, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, -} from './testHelpers'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -async function createNewUser( - program, - context: BankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -) { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await context.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - context, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: context.connection.toConnection(), - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - // @ts-ignore - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -describe('trading liquidity providing', () => { - const chProgram = anchor.workspace.Drift as Program; - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(300).mul(new BN(1e13)); - const ammInitialQuoteAssetReserve = new BN(300).mul(new BN(1e13)); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = new anchor.BN(1 * 10 ** 13).mul( - mantissaSqrtScale - ); - const stableAmmInitialBaseAssetReserve = new anchor.BN(1 * 10 ** 13).mul( - mantissaSqrtScale - ); - - const usdcAmount = new BN(1_000_000_000 * 1e6); - - let driftClient: TestClient; - - let bulkAccountLoader: TestBulkAccountLoader; - - let bankrunContextWrapper: BankrunContextWrapper; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let solusdc; - let solusdc2; - - before(async () => { - const context = await startAnchor('', [], []); - - bankrunContextWrapper = new BankrunContextWrapper(context); - - bulkAccountLoader = new TestBulkAccountLoader( - bankrunContextWrapper.connection, - 'processed', - 1 - ); - - usdcMint = await mockUSDCMint(bankrunContextWrapper); - - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - ]; - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - await driftClient.updatePerpMarketStepSizeAndTickSize( - 0, - new BN(1), - new BN(1) - ); - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updatePerpAuctionDuration(new BN(0)); - await driftClient.updatePerpMarketMarginRatio( - 0, - MARGIN_PRECISION.toNumber() / 2, - MARGIN_PRECISION.toNumber() / 4 - ); - - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - }); - - after(async () => { - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - }); - - it('lp trades with short', async () => { - let market = driftClient.getPerpMarketAccount(0); - - console.log('adding liquidity...'); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * 1e13), - market.marketIndex - ); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(40 * 1e13); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - ); - - await traderDriftClient.fetchAccounts(); - const position = traderDriftClient.getUserAccount().perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - assert(position.baseAssetAmount.gt(ZERO)); - - // settle says the lp would take on a short - const lpPosition = driftClientUser.getPerpPositionWithLPSettle(0)[0]; - console.log( - 'sdk settled lp position:', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString() - ); - assert(lpPosition.baseAssetAmount.lt(ZERO)); - assert(lpPosition.quoteAssetAmount.gt(ZERO)); - - // lp trades a big long - await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - // lp now has a long - const newLpPosition = driftClientUser.getUserAccount().perpPositions[0]; - console.log( - 'lp position:', - newLpPosition.baseAssetAmount.toString(), - newLpPosition.quoteAssetAmount.toString() - ); - assert(newLpPosition.baseAssetAmount.gt(ZERO)); - assert(newLpPosition.quoteAssetAmount.lt(ZERO)); - // is still an lp - assert(newLpPosition.lpShares.gt(ZERO)); - market = driftClient.getPerpMarketAccount(0); - - console.log('done!'); - }); -}); From b58cda0037f22098bc6c7c84e7d336e8c7c311aa Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 20 Jul 2025 18:16:59 -0400 Subject: [PATCH 006/159] init new margin calc --- programs/drift/src/math/margin.rs | 173 +++++++++++++++++- .../drift/src/state/margin_calculation.rs | 8 + programs/drift/src/state/perp_market.rs | 2 +- programs/drift/src/state/user.rs | 49 ++++- 4 files changed, 220 insertions(+), 12 deletions(-) diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index cc6af96add..2901be1b1c 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -28,6 +28,8 @@ use crate::state::user::{MarketType, OrderFillSimulation, PerpPosition, User}; use num_integer::Roots; use std::cmp::{max, min, Ordering}; +use super::spot_balance::get_token_amount; + #[cfg(test)] mod tests; @@ -147,8 +149,8 @@ pub fn calculate_perp_position_value_and_pnl( }; // add small margin requirement for every open order - margin_requirement = margin_requirement - .safe_add(market_position.margin_requirement_for_open_orders()?)?; + margin_requirement = + margin_requirement.safe_add(market_position.margin_requirement_for_open_orders()?)?; let unrealized_asset_weight = market.get_unrealized_asset_weight(total_unrealized_pnl, margin_requirement_type)?; @@ -233,6 +235,10 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_map: &mut OracleMap, context: MarginContext, ) -> DriftResult { + if context.isolated_position_market_index.is_some() { + return calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position(user, perp_market_map, spot_market_map, oracle_map, context); + } + let mut calculation = MarginCalculation::new(context); let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { @@ -494,6 +500,10 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( continue; } + if market_position.is_isolated_position() { + continue; + } + let market = &perp_market_map.get_ref(&market_position.market_index)?; validate!( @@ -634,6 +644,165 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( Ok(calculation) } +pub fn calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position( + user: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + context: MarginContext, +) -> DriftResult { + let mut calculation = MarginCalculation::new(context); + + let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { + user.max_margin_ratio + } else { + 0_u32 + }; + + if let Some(margin_ratio_override) = context.margin_ratio_override { + user_custom_margin_ratio = margin_ratio_override.max(user_custom_margin_ratio); + } + + let user_pool_id = user.pool_id; + let user_high_leverage_mode = user.is_high_leverage_mode(); + + let isolated_position_market_index = context.isolated_position_market_index.unwrap(); + + let perp_position = user.get_perp_position(isolated_position_market_index)?; + + let perp_market = perp_market_map.get_ref(&isolated_position_market_index)?; + + validate!( + user_pool_id == perp_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) == perp market pool id ({})", + user_pool_id, + perp_market.pool_id, + )?; + + let quote_spot_market = spot_market_map.get_ref(&perp_market.quote_spot_market_index)?; + + validate!( + user_pool_id == quote_spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) == quote spot market pool id ({})", + user_pool_id, + quote_spot_market.pool_id, + )?; + + let (quote_oracle_price_data, quote_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + quote_spot_market.market_index, + "e_spot_market.oracle_id(), + quote_spot_market + .historical_oracle_data + .last_oracle_price_twap, + quote_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + + let quote_oracle_valid = + is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; + + let quote_strict_oracle_price = StrictOraclePrice::new( + quote_oracle_price_data.price, + quote_spot_market + .historical_oracle_data + .last_oracle_price_twap_5min, + calculation.context.strict, + ); + quote_strict_oracle_price.validate()?; + + let quote_token_amount = get_token_amount( + perp_position + .isolated_position_scaled_balance + .cast::()?, + "e_spot_market, + &SpotBalanceType::Deposit, + )?; + + let quote_token_value = get_strict_token_value( + quote_token_amount.cast::()?, + quote_spot_market.decimals, + "e_strict_oracle_price, + )?; + + calculation.add_total_collateral(quote_token_value)?; + + calculation.update_all_deposit_oracles_valid(quote_oracle_valid); + + #[cfg(feature = "drift-rs")] + calculation.add_spot_asset_value(quote_token_value)?; + + let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Perp, + isolated_position_market_index, + &perp_market.oracle_id(), + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + perp_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + + let ( + perp_margin_requirement, + weighted_pnl, + worst_case_liability_value, + open_order_margin_requirement, + base_asset_value, + ) = calculate_perp_position_value_and_pnl( + &perp_position, + &perp_market, + oracle_price_data, + "e_strict_oracle_price, + context.margin_type, + user_custom_margin_ratio, + user_high_leverage_mode, + calculation.track_open_orders_fraction(), + )?; + + calculation.add_margin_requirement( + perp_margin_requirement, + worst_case_liability_value, + MarketIdentifier::perp(isolated_position_market_index), + )?; + + calculation.add_total_collateral(weighted_pnl)?; + + #[cfg(feature = "drift-rs")] + calculation.add_perp_liability_value(worst_case_liability_value)?; + #[cfg(feature = "drift-rs")] + calculation.add_perp_pnl(weighted_pnl)?; + + let has_perp_liability = perp_position.base_asset_amount != 0 + || perp_position.quote_asset_amount < 0 + || perp_position.has_open_order(); + + if has_perp_liability { + calculation.add_perp_liability()?; + calculation.update_with_perp_isolated_liability( + perp_market.contract_tier == ContractTier::Isolated, + ); + } + + if has_perp_liability || calculation.context.margin_type != MarginRequirementType::Initial { + calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( + quote_oracle_validity, + Some(DriftAction::MarginCalc), + )?); + calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( + oracle_validity, + Some(DriftAction::MarginCalc), + )?); + } + + calculation.validate_num_spot_liabilities()?; + + Ok(calculation) +} + pub fn validate_any_isolated_tier_requirements( user: &User, calculation: MarginCalculation, diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 4a0c299e4e..1756a5a7b8 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -35,6 +35,7 @@ pub struct MarginContext { pub fuel_perp_delta: Option<(u16, i64)>, pub fuel_spot_deltas: [(u16, i128); 2], pub margin_ratio_override: Option, + pub isolated_position_market_index: Option, } #[derive(PartialEq, Eq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] @@ -74,6 +75,7 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, + isolated_position_market_index: None, } } @@ -152,6 +154,7 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, + isolated_position_market_index: None, } } @@ -173,6 +176,11 @@ impl MarginContext { } Ok(self) } + + pub fn isolated_position_market_index(mut self, market_index: u16) -> Self { + self.isolated_position_market_index = Some(market_index); + self + } } #[derive(Clone, Copy, Debug)] diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 1adf030388..f33904c741 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -809,7 +809,7 @@ impl SpotBalance for PoolBalance { } fn update_balance_type(&mut self, _balance_type: SpotBalanceType) -> DriftResult { - Err(ErrorCode::CantUpdatePoolBalanceType) + Err(ErrorCode::CantUpdateSpotBalanceType) } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b32292bdbc..a31b7cc03d 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -11,8 +11,7 @@ use crate::math::orders::{ apply_protected_maker_limit_price_offset, standardize_base_asset_amount, standardize_price, }; use crate::math::position::{ - calculate_base_asset_value_and_pnl_with_oracle_price, - calculate_perp_liability_value, + calculate_base_asset_value_and_pnl_with_oracle_price, calculate_perp_liability_value, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{ @@ -21,7 +20,7 @@ use crate::math::spot_balance::{ use crate::math::stats::calculate_rolling_sum; use crate::msg; use crate::state::oracle::StrictOraclePrice; -use crate::state::perp_market::{ContractType}; +use crate::state::perp_market::ContractType; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::traits::Size; use crate::{get_then_update_id, ID, QUOTE_PRECISION_U64}; @@ -951,8 +950,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 @@ -965,7 +964,7 @@ pub struct PerpPosition { pub market_index: u16, /// The number of open orders pub open_orders: u8, - pub per_lp_base: i8, + pub position_type: u8, } impl PerpPosition { @@ -974,9 +973,7 @@ 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() } pub fn is_open_position(&self) -> bool { @@ -1120,6 +1117,40 @@ impl PerpPosition { None } } + + pub fn is_isolated_position(&self) -> bool { + self.position_type == 1 + } +} + +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]; From 820c2322c606cec509e36b843736f893f8264a2b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 23 Jul 2025 19:29:15 -0400 Subject: [PATCH 007/159] deposit and transfer into --- programs/drift/src/error.rs | 6 +- programs/drift/src/instructions/user.rs | 347 ++++++++++++++++++++++++ programs/drift/src/math/margin.rs | 2 +- programs/drift/src/state/user.rs | 25 +- 4 files changed, 376 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 61dee9f5f8..acaa63947a 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -193,8 +193,8 @@ pub enum ErrorCode { SpotMarketInsufficientDeposits, #[msg("UserMustSettleTheirOwnPositiveUnsettledPNL")] UserMustSettleTheirOwnPositiveUnsettledPNL, - #[msg("CantUpdatePoolBalanceType")] - CantUpdatePoolBalanceType, + #[msg("CantUpdateSpotBalanceType")] + CantUpdateSpotBalanceType, #[msg("InsufficientCollateralForSettlingPNL")] InsufficientCollateralForSettlingPNL, #[msg("AMMNotUpdatedInSameSlot")] @@ -639,6 +639,8 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid Isolated Perp Market")] + InvalidIsolatedPerpMarket, } #[macro_export] diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 80c3d7c013..36046801d5 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -16,6 +16,7 @@ use crate::controller::orders::{cancel_orders, ModifyOrderId}; use crate::controller::position::update_position_and_market; use crate::controller::position::PositionDirection; use crate::controller::spot_balance::update_revenue_pool_balances; +use crate::controller::spot_balance::update_spot_balances; use crate::controller::spot_position::{ update_spot_balances_and_cumulative_deposits, update_spot_balances_and_cumulative_deposits_with_limits, @@ -1896,6 +1897,300 @@ 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, DepositPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> Result<()> { + let user_key = ctx.accounts.user.key(); + let user = &mut 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)?; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + 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); + // TODO add back + // if user.is_being_liquidated() { + // // try to update liquidation status if user is was already being liq'd + // let is_being_liquidated = is_user_being_liquidated( + // user, + // &perp_market_map, + // &spot_market_map, + // &mut oracle_map, + // state.liquidation_margin_buffer_ratio, + // )?; + + // if !is_being_liquidated { + // user.exit_liquidation(); + // } + // } + + user.update_last_active_slot(slot); + + let spot_market = &mut spot_market_map.get_ref_mut(&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()?; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let oracle_price = oracle_price_data.price; + + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Deposit, + amount, + oracle_price, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after, + total_withdraws_after, + market_index: spot_market_index, + explanation: DepositExplanation::None, + transfer_user: None, + }; + emit!(deposit_record); + + 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_deposit_into_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, TransferDepositIntoIsolatedPerpPosition<'info>>, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> 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), + )?; + + { + 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), + clock.unix_timestamp, + )?; + } + + { + 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 spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; + update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + } + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + )?; + + validate_spot_margin_trading( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; + + if user.is_being_liquidated() { + user.exit_liquidation(); + } + + user.update_last_active_slot(slot); + + { + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + validate!( + user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + spot_market, + perp_position, + false, + )?; + } + + 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( exchange_not_paused(&ctx.accounts.state) )] @@ -4330,6 +4625,58 @@ pub struct CancelOrder<'info> { pub authority: Signer<'info>, } +#[derive(Accounts)] +#[instruction(spot_market_index: u16,)] +pub struct DepositPerpPosition<'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 TransferDepositIntoIsolatedPerpPosition<'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)] pub struct PlaceAndTake<'info> { pub state: Box>, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 2901be1b1c..126851057a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -500,7 +500,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( continue; } - if market_position.is_isolated_position() { + if market_position.is_isolated() { continue; } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index a31b7cc03d..42eafe858e 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -257,6 +257,29 @@ 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_type = 1; + + Ok(&mut self.perp_positions[position_index]) + } + } + pub fn get_order_index(&self, order_id: u32) -> DriftResult { self.orders .iter() @@ -1118,7 +1141,7 @@ impl PerpPosition { } } - pub fn is_isolated_position(&self) -> bool { + pub fn is_isolated(&self) -> bool { self.position_type == 1 } } From 9efd808c0e34a6a7f340b0d49aec754315ae65ad Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 23 Jul 2025 19:40:39 -0400 Subject: [PATCH 008/159] add settle pnl --- programs/drift/src/controller/pnl.rs | 53 +++++++++++++++++++++------- programs/drift/src/state/user.rs | 4 +++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 46cf1e9779..bd4427e9fd 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -85,8 +85,9 @@ pub fn settle_pnl( if unrealized_pnl < 0 { // may already be cached let meets_margin_requirement = match meets_margin_requirement { - Some(meets_margin_requirement) => meets_margin_requirement, - None => meets_settle_pnl_maintenance_margin_requirement( + Some(meets_margin_requirement) if !user.perp_positions[position_index].is_isolated() => meets_margin_requirement, + // TODO check margin for isolate position + _ => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, @@ -268,17 +269,43 @@ pub fn settle_pnl( ); } - update_spot_balances( - pnl_to_settle_with_user.unsigned_abs(), - if pnl_to_settle_with_user > 0 { - &SpotBalanceType::Deposit - } else { - &SpotBalanceType::Borrow - }, - spot_market, - user.get_quote_spot_position_mut(), - false, - )?; + if user.perp_positions[position_index].is_isolated() { + let perp_position = &mut user.perp_positions[position_index]; + if pnl_to_settle_with_user < 0 { + let token_amount = perp_position.get_isolated_position_token_amount(spot_market)?; + + validate!( + token_amount >= pnl_to_settle_with_user.unsigned_abs(), + ErrorCode::InsufficientCollateralForSettlingPNL, + "user has insufficient deposit for market {}", + market_index + )?; + } + + update_spot_balances( + pnl_to_settle_with_user.unsigned_abs(), + if pnl_to_settle_with_user > 0 { + &SpotBalanceType::Deposit + } else { + &SpotBalanceType::Borrow + }, + spot_market, + perp_position, + false, + )?; + } else { + update_spot_balances( + pnl_to_settle_with_user.unsigned_abs(), + if pnl_to_settle_with_user > 0 { + &SpotBalanceType::Deposit + } else { + &SpotBalanceType::Borrow + }, + spot_market, + user.get_quote_spot_position_mut(), + false, + )?; + } update_quote_asset_amount( &mut user.perp_positions[position_index], diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 42eafe858e..2a474de4c5 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1144,6 +1144,10 @@ impl PerpPosition { pub fn is_isolated(&self) -> bool { self.position_type == 1 } + + pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + get_token_amount(self.isolated_position_scaled_balance as u128, spot_market, &SpotBalanceType::Deposit) + } } impl SpotBalance for PerpPosition { From 75b92f89a220009d3155796209427025798a2be1 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 24 Jul 2025 09:28:41 -0400 Subject: [PATCH 009/159] program: add withdraw --- programs/drift/src/instructions/user.rs | 187 ++++++++++++++++++++++++ programs/drift/src/state/user.rs | 6 + 2 files changed, 193 insertions(+) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 36046801d5..a17e38edf4 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -786,6 +786,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( amount as u128, &mut user_stats, now, + None, )?; validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; @@ -960,6 +961,7 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( amount as u128, user_stats, now, + None, )?; validate_spot_margin_trading( @@ -2144,6 +2146,7 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( amount as u128, user_stats, now, + None, )?; validate_spot_margin_trading( @@ -2190,6 +2193,156 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( Ok(()) } +#[access_control( + withdraw_not_paused(&ctx.accounts.state) +)] +pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, Withdraw<'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)?; + + 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_position_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, + )?; + } + + // this is wrong + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + &mut user_stats, + now, + Some(perp_market_index), + )?; + + // TODO figure out what to do here + // if user.is_being_liquidated() { + // user.exit_liquidation(); + // } + + user.update_last_active_slot(slot); + + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price = oracle_map.get_price_data(&spot_market.oracle_id())?.price; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Withdraw, + oracle_price, + amount, + market_index: spot_market_index, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after: user.total_deposits, + total_withdraws_after: user.total_withdraws, + explanation: DepositExplanation::None, + transfer_user: None, + }; + emit!(deposit_record); + + 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) @@ -4677,6 +4830,40 @@ pub struct TransferDepositIntoIsolatedPerpPosition<'info> { pub spot_market_vault: Box>, } +#[derive(Accounts)] +#[instruction(spot_market_index: u16)] +pub struct WithdrawFromIsolatedPerpPosition<'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/state/user.rs b/programs/drift/src/state/user.rs index 2a474de4c5..c38d81291f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -587,6 +587,7 @@ impl User { withdraw_amount: u128, user_stats: &mut UserStats, now: i64, + isolated_perp_position_market_index: Option, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; let context = MarginContext::standard(margin_requirement_type) @@ -595,6 +596,11 @@ impl User { .fuel_spot_delta(withdraw_market_index, withdraw_amount.cast::()?) .fuel_numerator(self, now); + // TODO check if this is correct + if let Some(isolated_perp_position_market_index) = isolated_perp_position_market_index { + context.isolated_position_market_index(isolated_perp_position_market_index); + } + let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, perp_market_map, From 162fc23d08c9a6464996233f6be622849d7d4460 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 24 Jul 2025 09:48:46 -0400 Subject: [PATCH 010/159] add more ix --- programs/drift/src/instructions/user.rs | 152 +++++++++++++++--------- programs/drift/src/lib.rs | 27 +++++ 2 files changed, 125 insertions(+), 54 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a17e38edf4..fbb883d72b 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1903,7 +1903,7 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( deposit_not_paused(&ctx.accounts.state) )] pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, DepositPerpPosition<'info>>, + ctx: Context<'_, '_, 'c, 'info, DepositIsolatedPerpPosition<'info>>, spot_market_index: u16, perp_market_index: u16, amount: u64, @@ -2064,11 +2064,11 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( deposit_not_paused(&ctx.accounts.state) withdraw_not_paused(&ctx.accounts.state) )] -pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, TransferDepositIntoIsolatedPerpPosition<'info>>, +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: u64, + amount: i64, ) -> anchor_lang::Result<()> { let authority_key = ctx.accounts.authority.key; let user_key = ctx.accounts.user.key(); @@ -2101,18 +2101,9 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - { - 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), - clock.unix_timestamp, - )?; - } - { 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, @@ -2121,69 +2112,122 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( 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), + clock.unix_timestamp, + )?; } - { - let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + 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, - spot_market, + &mut spot_market, &mut user.spot_positions[spot_position_index], false, None, )?; - } - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( - &perp_market_map, - &spot_market_map, - &mut oracle_map, - MarginRequirementType::Initial, - spot_market_index, - amount as u128, - user_stats, - now, - None, - )?; + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, + false, + )?; - validate_spot_margin_trading( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + drop(spot_market); - if user.is_being_liquidated() { - user.exit_liquidation(); - } + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + None, + )?; - user.update_last_active_slot(slot); + validate_spot_margin_trading( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; - { - let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + if user.is_being_liquidated() { + user.exit_liquidation(); + } + } 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_position_token_amount(&spot_market)?; validate!( - user.pool_id == spot_market.pool_id, - ErrorCode::InvalidPoolId, - "user pool id ({}) != market pool id ({})", - user.pool_id, - spot_market.pool_id + amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + spot_market_index )?; - let perp_position = user.force_get_isolated_perp_position_mut(perp_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::Deposit, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; update_spot_balances( amount as u128, - &SpotBalanceType::Deposit, - spot_market, - perp_position, + &SpotBalanceType::Borrow, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, false, )?; + + drop(spot_market); + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + Some(perp_market_index), + )?; + + // TODO figure out what to do here + // if user.is_being_liquidated() { + // user.exit_liquidation(); + // } } + + + user.update_last_active_slot(slot); + let spot_market = spot_market_map.get_ref(&spot_market_index)?; math::spot_withdraw::validate_spot_market_vault_amount( &spot_market, @@ -2197,7 +2241,7 @@ pub fn handle_transfer_deposit_into_isolated_perp_position<'c: 'info, 'info>( withdraw_not_paused(&ctx.accounts.state) )] pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, Withdraw<'info>>, + ctx: Context<'_, '_, 'c, 'info, WithdrawIsolatedPerpPosition<'info>>, spot_market_index: u16, perp_market_index: u16, amount: u64, @@ -4780,7 +4824,7 @@ pub struct CancelOrder<'info> { #[derive(Accounts)] #[instruction(spot_market_index: u16,)] -pub struct DepositPerpPosition<'info> { +pub struct DepositIsolatedPerpPosition<'info> { pub state: Box>, #[account( mut, @@ -4810,7 +4854,7 @@ pub struct DepositPerpPosition<'info> { #[derive(Accounts)] #[instruction(spot_market_index: u16,)] -pub struct TransferDepositIntoIsolatedPerpPosition<'info> { +pub struct TransferIsolatedPerpPositionDeposit<'info> { #[account( mut, constraint = can_sign_for_user(&user, &authority)? @@ -4832,7 +4876,7 @@ pub struct TransferDepositIntoIsolatedPerpPosition<'info> { #[derive(Accounts)] #[instruction(spot_market_index: u16)] -pub struct WithdrawFromIsolatedPerpPosition<'info> { +pub struct WithdrawIsolatedPerpPosition<'info> { pub state: Box>, #[account( mut, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index cedcbfbfeb..5f172e3043 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -168,6 +168,33 @@ 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, From 82463f3b351f4f54af35fd0169bf2ac01f2efd11 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 26 Jul 2025 18:09:16 -0400 Subject: [PATCH 011/159] add new meets withdraw req fn --- programs/drift/src/instructions/user.rs | 6 ++-- programs/drift/src/state/user.rs | 42 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index fbb883d72b..eb4f5bbe60 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2206,16 +2206,14 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( drop(spot_market); - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + user.meets_withdraw_margin_requirement_for_isolated_perp_position( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - spot_market_index, - amount as u128, user_stats, now, - Some(perp_market_index), + perp_market_index, )?; // TODO figure out what to do here diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index c38d81291f..0bb2d212e1 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -638,6 +638,48 @@ impl User { Ok(true) } + pub fn meets_withdraw_margin_requirement_for_isolated_perp_position( + &mut self, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + margin_requirement_type: MarginRequirementType, + user_stats: &mut UserStats, + now: i64, + isolated_perp_position_market_index: u16, + ) -> DriftResult { + let strict = margin_requirement_type == MarginRequirementType::Initial; + let context = MarginContext::standard(margin_requirement_type) + .strict(strict) + .isolated_position_market_index(isolated_perp_position_market_index); + + let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + self, + perp_market_map, + spot_market_map, + oracle_map, + context, + )?; + + if calculation.margin_requirement > 0 || calculation.get_num_of_liabilities()? > 0 { + validate!( + calculation.all_liability_oracles_valid, + ErrorCode::InvalidOracle, + "User attempting to withdraw with outstanding liabilities when an oracle is invalid" + )?; + } + + validate!( + calculation.meets_margin_requirement(), + ErrorCode::InsufficientCollateral, + "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", + calculation.total_collateral, + calculation.margin_requirement + )?; + + Ok(true) + } + pub fn can_skip_auction_duration(&self, user_stats: &UserStats) -> DriftResult { if user_stats.disable_update_perp_bid_ask_twap { return Ok(false); From fb57e5f0e44e73158254a66d05bd145c1a8fe096 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 26 Jul 2025 20:06:49 -0400 Subject: [PATCH 012/159] enter/exit liquidation logic --- programs/drift/src/instructions/user.rs | 102 +++++++++++++++--------- programs/drift/src/math/liquidation.rs | 21 +++++ programs/drift/src/state/user.rs | 46 ++++++++++- 3 files changed, 130 insertions(+), 39 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index eb4f5bbe60..0e9c8cae84 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -33,6 +33,7 @@ use crate::instructions::optional_accounts::{ }; use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; +use crate::math::liquidation::is_isolated_position_being_liquidated; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; @@ -1980,15 +1981,17 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( 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)?; + { + 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, - )?; + update_spot_balances( + amount.cast::()?, + &SpotBalanceType::Deposit, + &mut spot_market, + perp_position, + false, + )?; + } validate!( matches!(spot_market.status, MarketStatus::Active), @@ -1997,21 +2000,22 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( )?; drop(spot_market); - // TODO add back - // if user.is_being_liquidated() { - // // try to update liquidation status if user is was already being liq'd - // let is_being_liquidated = is_user_being_liquidated( - // user, - // &perp_market_map, - // &spot_market_map, - // &mut oracle_map, - // state.liquidation_margin_buffer_ratio, - // )?; - - // if !is_being_liquidated { - // user.exit_liquidation(); - // } - // } + + if user.is_isolated_position_being_liquidated(perp_market_index)? { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_isolated_position_being_liquidated( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + perp_market_index, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_isolated_position_liquidation(perp_market_index)?; + } + } user.update_last_active_slot(slot); @@ -2174,6 +2178,22 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( if user.is_being_liquidated() { user.exit_liquidation(); } + + if user.is_isolated_position_being_liquidated(perp_market_index)? { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_isolated_position_being_liquidated( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + perp_market_index, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_isolated_position_liquidation(perp_market_index)?; + } + } } else { let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; @@ -2216,10 +2236,24 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( perp_market_index, )?; - // TODO figure out what to do here - // if user.is_being_liquidated() { - // user.exit_liquidation(); - // } + if user.is_isolated_position_being_liquidated(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } + + if user.is_being_liquidated() { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_user_being_liquidated( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_liquidation(); + } + } } @@ -2315,23 +2349,19 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( )?; } - // this is wrong - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + user.meets_withdraw_margin_requirement_for_isolated_perp_position( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - spot_market_index, - amount as u128, &mut user_stats, now, - Some(perp_market_index), + perp_market_index, )?; - // TODO figure out what to do here - // if user.is_being_liquidated() { - // user.exit_liquidation(); - // } + if user.is_isolated_position_being_liquidated(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } user.update_last_active_slot(slot); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 24a54afc59..4035719290 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -246,6 +246,27 @@ pub fn validate_user_not_being_liquidated( Ok(()) } +pub fn is_isolated_position_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).isolated_position_market_index(perp_market_index), + )?; + + let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; + + Ok(is_being_liquidated) +} + pub enum LiquidationMultiplierType { Discount, Premium, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 0bb2d212e1..ef5a2e7c55 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -274,12 +274,23 @@ impl User { 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_type = 1; + 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() @@ -386,6 +397,29 @@ impl User { self.liquidation_margin_freed = 0; } + pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + if self.is_isolated_position_being_liquidated(perp_market_index)? { + return self.next_liquidation_id.safe_sub(1); + } + + let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; + + perp_position.position_flag |= PositionFlag::BeingLiquidated as u8; + + Ok(get_then_update_id!(self, next_liquidation_id)) + } + + pub fn exit_isolated_position_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); + Ok(()) + } + + pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { + let perp_position = self.get_isolated_perp_position(perp_market_index)?; + Ok(perp_position.position_flag & PositionFlag::BeingLiquidated 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(()) @@ -1035,7 +1069,7 @@ pub struct PerpPosition { pub market_index: u16, /// The number of open orders pub open_orders: u8, - pub position_type: u8, + pub position_flag: u8, } impl PerpPosition { @@ -1190,7 +1224,7 @@ impl PerpPosition { } pub fn is_isolated(&self) -> bool { - self.position_type == 1 + self.position_flag & PositionFlag::IsolatedPosition as u8 == PositionFlag::IsolatedPosition as u8 } pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { @@ -1691,6 +1725,12 @@ pub enum OrderBitFlag { SafeTriggerOrder = 0b00000100, } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum PositionFlag { + IsolatedPosition = 0b00000001, + BeingLiquidated = 0b00000010, +} + #[account(zero_copy(unsafe))] #[derive(Eq, PartialEq, Debug)] #[repr(C)] From 4de579a96ce812b747dc465e3dcecc651125d1c4 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 26 Jul 2025 20:40:01 -0400 Subject: [PATCH 013/159] moar --- programs/drift/src/controller/liquidation.rs | 6 +-- programs/drift/src/controller/orders.rs | 20 ++++++++-- programs/drift/src/controller/pnl.rs | 17 +++++++- programs/drift/src/controller/pnl/tests.rs | 3 +- programs/drift/src/instructions/keeper.rs | 27 +++++++++---- programs/drift/src/instructions/user.rs | 33 ++++++++++++---- programs/drift/src/math/margin.rs | 41 ++++++++++++++++++-- programs/drift/src/state/user.rs | 1 + 8 files changed, 121 insertions(+), 27 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 137bc1d055..f722025728 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -556,7 +556,7 @@ pub fn liquidate_perp( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; validate!( liquidator_meets_initial_margin_requirement, @@ -2706,7 +2706,7 @@ pub fn liquidate_borrow_for_perp_pnl( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; validate!( liquidator_meets_initial_margin_requirement, @@ -3207,7 +3207,7 @@ pub fn liquidate_perp_pnl_for_deposit( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; validate!( liquidator_meets_initial_margin_requirement, diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 4502129845..bc7cc76ad9 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -356,14 +356,21 @@ pub fn place_perp_order( options.update_risk_increasing(risk_increasing); + let isolated_position_market_index = if user.perp_positions[position_index].is_isolated() { + Some(market_index) + } else { + None + }; + // when orders are placed in bulk, only need to check margin on last place - if options.enforce_margin_check && !options.is_liquidation() { + if (options.enforce_margin_check || isolated_position_market_index.is_some()) && !options.is_liquidation() { meets_place_order_margin_requirement( user, perp_market_map, spot_market_map, oracle_map, options.risk_increasing, + isolated_position_market_index, )?; } @@ -3072,8 +3079,14 @@ pub fn trigger_order( // If order increases risk and user is below initial margin, cancel it if is_risk_increasing && !user.orders[order_index].reduce_only { + let isolated_position_market_index = if user.get_perp_position(market_index)?.is_isolated() { + Some(market_index) + } else { + None + }; + let meets_initial_margin_requirement = - meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, isolated_position_market_index)?; if !meets_initial_margin_requirement { cancel_order( @@ -3571,6 +3584,7 @@ pub fn place_spot_order( spot_market_map, oracle_map, options.risk_increasing, + None, )?; } @@ -5331,7 +5345,7 @@ pub fn trigger_spot_order( // If order is risk increasing and user is below initial margin, cancel it if is_risk_increasing && !user.orders[order_index].reduce_only { let meets_initial_margin_requirement = - meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; + meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, None)?; if !meets_initial_margin_requirement { cancel_order( diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index bd4427e9fd..01dd0a55d4 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -83,15 +83,22 @@ pub fn settle_pnl( // cannot settle negative pnl this way on a user who is in liquidation territory if unrealized_pnl < 0 { + let isolated_position_market_index = if user.perp_positions[position_index].is_isolated() { + Some(market_index) + } else { + None + }; + // may already be cached let meets_margin_requirement = match meets_margin_requirement { - Some(meets_margin_requirement) if !user.perp_positions[position_index].is_isolated() => meets_margin_requirement, + Some(meets_margin_requirement) if !isolated_position_market_index.is_some() => meets_margin_requirement, // TODO check margin for isolate position _ => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, oracle_map, + isolated_position_market_index, )?, }; @@ -351,8 +358,14 @@ pub fn settle_expired_position( ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + let isolated_position_market_index = if user.get_perp_position(perp_market_index)?.is_isolated() { + Some(perp_market_index) + } else { + None + }; + // cannot settle pnl this way on a user who is in liquidation territory - if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) + if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, isolated_position_market_index)?) { return Err(ErrorCode::InsufficientCollateralForSettlingPNL); } diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index 4a35df4e49..ee6dd872b7 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -400,7 +400,7 @@ pub fn user_does_not_meet_strict_maintenance_requirement() { assert_eq!(result, Err(ErrorCode::InsufficientCollateralForSettlingPNL)); let meets_maintenance = - meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map) + meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map, None) .unwrap(); assert_eq!(meets_maintenance, true); @@ -410,6 +410,7 @@ pub fn user_does_not_meet_strict_maintenance_requirement() { &market_map, &spot_market_map, &mut oracle_map, + None, ) .unwrap(); diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 222ec30755..f62df6fb1d 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -989,12 +989,25 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + let mut try_cache_margin_requirement = false; + for market_index in market_indexes.iter() { + if !user.get_perp_position(*market_index)?.is_isolated() { + try_cache_margin_requirement = true; + break; + } + } + + let meets_margin_requirement = if try_cache_margin_requirement { + Some(meets_settle_pnl_maintenance_margin_requirement( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + None, + )?) + } else { + None + }; for market_index in market_indexes.iter() { let market_in_settlement = @@ -1035,7 +1048,7 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( &mut oracle_map, &clock, state, - Some(meets_margin_requirement), + meets_margin_requirement, mode, ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 0e9c8cae84..a8e6ace067 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -39,7 +39,7 @@ use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_l use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ calculate_max_withdrawable_amount, meets_maintenance_margin_requirement, - meets_place_order_margin_requirement, validate_spot_margin_trading, MarginRequirementType, + validate_spot_margin_trading, MarginRequirementType, }; use crate::math::oracle::is_oracle_valid_for_action; use crate::math::oracle::DriftAction; @@ -3515,7 +3515,7 @@ pub fn handle_update_user_pool_id<'c: 'info, 'info>( user.pool_id = pool_id; // will throw if user has deposits/positions in other pools - meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; + meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map, None)?; Ok(()) } @@ -3741,12 +3741,29 @@ pub fn handle_enable_user_high_leverage_mode<'c: 'info, 'info>( "user already in high leverage mode" )?; - meets_maintenance_margin_requirement( - &user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + let has_non_isolated_position = user.perp_positions.iter().any(|position| !position.is_isolated()); + + if has_non_isolated_position { + meets_maintenance_margin_requirement( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + None, + )?; + } + + let isolated_position_market_indexes = user.perp_positions.iter().filter(|position| position.is_isolated()).map(|position| position.market_index).collect::>(); + + for market_index in isolated_position_market_indexes.iter() { + meets_maintenance_margin_requirement( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + Some(*market_index), + )?; + } let mut config = load_mut!(ctx.accounts.high_leverage_mode_config)?; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 126851057a..aa81af0add 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -228,6 +228,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, @@ -848,6 +849,7 @@ pub fn meets_place_order_margin_requirement( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, risk_increasing: bool, + isolated_position_market_index: Option, ) -> DriftResult { let margin_type = if risk_increasing { MarginRequirementType::Initial @@ -856,6 +858,10 @@ pub fn meets_place_order_margin_requirement( }; let context = MarginContext::standard(margin_type).strict(true); + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, @@ -884,13 +890,20 @@ pub fn meets_initial_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, + isolated_position_market_index: Option, ) -> DriftResult { + let context = MarginContext::standard(MarginRequirementType::Initial); + + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Initial), + context, ) .map(|calc| calc.meets_margin_requirement()) } @@ -900,13 +913,20 @@ pub fn meets_settle_pnl_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, + isolated_position_market_index: Option, ) -> DriftResult { + let context = MarginContext::standard(MarginRequirementType::Maintenance).strict(true); + + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance).strict(true), + context, ) .map(|calc| calc.meets_margin_requirement()) } @@ -916,13 +936,20 @@ pub fn meets_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, + isolated_position_market_index: Option, ) -> DriftResult { + let context = MarginContext::standard(MarginRequirementType::Maintenance); + + if let Some(isolated_position_market_index) = isolated_position_market_index { + let context = context.isolated_position_market_index(isolated_position_market_index); + } + calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance), + context, ) .map(|calc| calc.meets_margin_requirement()) } @@ -1110,6 +1137,14 @@ 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_position_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/state/user.rs b/programs/drift/src/state/user.rs index ef5a2e7c55..b5a0d5cd76 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -398,6 +398,7 @@ impl User { } pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + // todo figure out liquidation id if self.is_isolated_position_being_liquidated(perp_market_index)? { return self.next_liquidation_id.safe_sub(1); } From 085e8057076c89eba7ff7efff4cf5569cbf998e2 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 30 Jul 2025 17:34:06 -0400 Subject: [PATCH 014/159] start liquidation logic --- programs/drift/src/controller/liquidation.rs | 53 ++++--- programs/drift/src/controller/orders.rs | 9 ++ programs/drift/src/controller/pnl.rs | 1 + programs/drift/src/instructions/keeper.rs | 7 + programs/drift/src/instructions/user.rs | 1 + programs/drift/src/math/bankruptcy.rs | 10 ++ programs/drift/src/state/liquidation_mode.rs | 153 +++++++++++++++++++ programs/drift/src/state/mod.rs | 1 + programs/drift/src/state/user.rs | 16 +- 9 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 programs/drift/src/state/liquidation_mode.rs diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index f722025728..390e4a8c8b 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1,6 +1,7 @@ use std::ops::{Deref, DerefMut}; use crate::msg; +use crate::state::liquidation_mode::{get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode}; use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; @@ -139,20 +140,22 @@ pub fn liquidate_perp( now, )?; + let liquidation_mode = get_perp_liquidation_mode(user, market_index); + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + liquidation_mode.get_margin_context(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 && margin_calculation.meets_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_being_liquidated && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -184,6 +187,7 @@ pub fn liquidate_perp( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; + let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -194,9 +198,10 @@ pub fn liquidate_perp( now, slot, OrderActionExplanation::Liquidation, + cancel_order_market_type, + cancel_order_market_index, None, - None, - None, + cancel_order_skip_isolated_positions, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -219,19 +224,16 @@ 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 margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + margin_context, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -257,13 +259,13 @@ pub fn liquidate_perp( liquidate_perp: LiquidatePerpRecord { market_index, oracle_price, - lp_shares, + lp_shares: 0, ..LiquidatePerpRecord::default() }, ..LiquidationRecord::default() }); - user.exit_liquidation(); + liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -328,6 +330,7 @@ pub fn liquidate_perp( .get_price_data("e_spot_market.oracle_id())? .price; + // todo how to handle slot not being on perp position? let liquidator_fee = get_liquidation_fee( market.get_base_liquidator_fee(user.is_high_leverage_mode()), market.get_max_liquidation_fee()?, @@ -365,7 +368,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, @@ -545,18 +548,21 @@ pub fn liquidate_perp( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; 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.is_user_bankrupt(user)? { + liquidation_mode.enter_bankruptcy(user); } + let liquidator_isolated_position_market_index = liquidator.get_perp_position(market_index)?.is_isolated().then_some(market_index); + let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; validate!( liquidator_meets_initial_margin_requirement, @@ -688,7 +694,7 @@ 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, @@ -3630,6 +3636,7 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, + margin_context: MarginContext, ) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( @@ -3637,7 +3644,7 @@ pub fn calculate_margin_freed( perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), + margin_context, )?; let new_margin_shortage = margin_calculation_after.margin_shortage()?; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index bc7cc76ad9..a1cab6290c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -526,8 +526,10 @@ 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; @@ -541,6 +543,8 @@ 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 { @@ -3237,6 +3241,11 @@ pub fn force_cancel_orders( continue; } + // TODO: handle force deleting these orders + if user.get_perp_position(market_index)?.is_isolated() { + continue; + } + state.perp_fee_structure.flat_filler_fee } }; diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 01dd0a55d4..cff0b45ccb 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -399,6 +399,7 @@ pub fn settle_expired_position( Some(MarketType::Perp), Some(perp_market_index), None, + true, )?; let position_index = match get_position_index(&user.perp_positions, perp_market_index) { diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index f62df6fb1d..a272f8e06e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2858,6 +2858,13 @@ pub fn handle_force_delete_user<'c: 'info, 'info>( None, None, None, + false, + )?; + + validate!( + !user.perp_positions.iter().any(|p| !p.is_available()), + ErrorCode::DefaultError, + "user must have no perp positions" )?; for spot_position in user.spot_positions.iter_mut() { diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a8e6ace067..ec68ed4306 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2614,6 +2614,7 @@ pub fn handle_cancel_orders<'c: 'info, 'info>( market_type, market_index, direction, + true, )?; Ok(()) diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 287b103060..6e152857af 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -33,3 +33,13 @@ pub fn is_user_bankrupt(user: &User) -> bool { has_liability } + +pub fn is_user_isolated_position_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()); +} \ No newline at end of file diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs new file mode 100644 index 0000000000..6648417692 --- /dev/null +++ b/programs/drift/src/state/liquidation_mode.rs @@ -0,0 +1,153 @@ +use crate::{error::DriftResult, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate}, state::margin_calculation::{MarginContext, MarketIdentifier}, LIQUIDATION_PCT_PRECISION}; + +use super::user::{MarketType, User}; + +pub trait LiquidatePerpMode { + fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult; + + fn user_is_being_liquidated(&self, user: &User) -> DriftResult; + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; + + fn get_cancel_orders_params(&self) -> (Option, Option, bool); + + 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); + + fn is_user_bankrupt(&self, user: &User) -> DriftResult; + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()>; + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; +} + +pub fn get_perp_liquidation_mode(user: &User, market_index: u16) -> Box { + Box::new(CrossMarginLiquidatePerpMode::new(market_index)) +} + +pub struct CrossMarginLiquidatePerpMode { + pub market_index: u16, +} + +impl CrossMarginLiquidatePerpMode { + pub fn new(market_index: u16) -> Self { + Self { market_index } + } +} + +impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { + fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + } + + fn user_is_being_liquidated(&self, user: &User) -> DriftResult { + Ok(user.is_being_liquidated()) + } + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { + Ok(user.exit_liquidation()) + } + + fn get_cancel_orders_params(&self) -> (Option, Option, bool) { + (None, None, true) + } + + 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) { + user.increment_margin_freed(amount); + } + + fn is_user_bankrupt(&self, user: &User) -> DriftResult { + Ok(is_user_bankrupt(user)) + } + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + Ok(user.enter_bankruptcy()) + } + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + Ok(user.exit_bankruptcy()) + } +} + +pub struct IsolatedLiquidatePerpMode { + pub market_index: u16, +} + +impl IsolatedLiquidatePerpMode { + pub fn new(market_index: u16) -> Self { + Self { market_index } + } +} + +impl LiquidatePerpMode for IsolatedLiquidatePerpMode { + fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .isolated_position_market_index(self.market_index) + .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + } + + fn user_is_being_liquidated(&self, user: &User) -> DriftResult { + user.is_isolated_position_being_liquidated(self.market_index) + } + + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { + user.exit_isolated_position_liquidation(self.market_index) + } + + fn get_cancel_orders_params(&self) -> (Option, Option, bool) { + (Some(MarketType::Perp), Some(self.market_index), true) + } + + 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) { + return; + } + + fn is_user_bankrupt(&self, user: &User) -> DriftResult { + is_user_isolated_position_bankrupt(user, self.market_index) + } + + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + user.enter_isolated_position_bankruptcy(self.market_index) + } + + fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { + user.exit_isolated_position_bankruptcy(self.market_index) + } +} \ No newline at end of file diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index a9c9724757..65fdacf16d 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -5,6 +5,7 @@ pub mod fulfillment_params; pub mod high_leverage_mode_config; pub mod if_rebalance_config; pub mod insurance_fund_stake; +pub mod liquidation_mode; pub mod load_ref; pub mod margin_calculation; pub mod oracle; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b5a0d5cd76..e0fc445204 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -418,7 +418,20 @@ impl User { pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.position_flag & PositionFlag::BeingLiquidated as u8 != 0) + Ok(perp_position.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0) + } + + pub fn enter_isolated_position_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::Bankruptcy as u8; + Ok(()) + } + + pub fn exit_isolated_position_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::Bankruptcy as u8); + Ok(()) } pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { @@ -1730,6 +1743,7 @@ pub enum OrderBitFlag { pub enum PositionFlag { IsolatedPosition = 0b00000001, BeingLiquidated = 0b00000010, + Bankruptcy = 0b00000100, } #[account(zero_copy(unsafe))] From 4e7db0fac9c6b2a4510fa98a719f47de2a70dc6d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 30 Jul 2025 19:13:50 -0400 Subject: [PATCH 015/159] other liquidation fns --- programs/drift/src/controller/liquidation.rs | 152 +++++++++---------- programs/drift/src/state/liquidation_mode.rs | 145 +++++++++++++++++- programs/drift/src/state/user.rs | 5 + 3 files changed, 216 insertions(+), 86 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 390e4a8c8b..37cf9cd0f6 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -96,8 +96,10 @@ pub fn liquidate_perp( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; + let liquidation_mode = get_perp_liquidation_mode(user, market_index); + validate!( - !user.is_bankrupt(), + !liquidation_mode.is_user_bankrupt(user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -140,14 +142,12 @@ pub fn liquidate_perp( now, )?; - let liquidation_mode = get_perp_liquidation_mode(user, market_index); - let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(user)?; @@ -226,7 +226,7 @@ pub fn liquidate_perp( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -242,7 +242,7 @@ pub fn liquidate_perp( 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()? { emit!(LiquidationRecord { @@ -555,7 +555,7 @@ pub fn liquidate_perp( if base_asset_amount >= base_asset_amount_to_cover_margin_shortage { liquidation_mode.exit_liquidation(user)?; - } else if liquidation_mode.is_user_bankrupt(user)? { + } else if liquidation_mode.should_user_enter_bankruptcy(user)? { liquidation_mode.enter_bankruptcy(user); } @@ -733,8 +733,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", )?; @@ -777,20 +779,20 @@ pub fn liquidate_perp_with_fill( now, )?; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + margin_context, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_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 liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -811,7 +813,8 @@ pub fn liquidate_perp_with_fill( || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - + + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( &mut user, user_key, @@ -822,9 +825,10 @@ pub fn liquidate_perp_with_fill( now, slot, OrderActionExplanation::Liquidation, + cancel_orders_market_type, + cancel_orders_market_index, None, - None, - None, + cancel_orders_is_isolated, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -847,19 +851,16 @@ 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 margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + margin_context, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -868,7 +869,7 @@ pub fn liquidate_perp_with_fill( 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()? { emit!(LiquidationRecord { @@ -885,7 +886,7 @@ pub fn liquidate_perp_with_fill( liquidate_perp: LiquidatePerpRecord { market_index, oracle_price, - lp_shares, + lp_shares: 0, ..LiquidatePerpRecord::default() }, ..LiquidationRecord::default() @@ -963,7 +964,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, @@ -1104,15 +1105,16 @@ pub fn liquidate_perp_with_fill( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; 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 margin_calculation_after.meets_margin_requirement() { - 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 user_position_delta = get_position_delta_for_fill( @@ -1137,7 +1139,7 @@ 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, @@ -1672,6 +1674,8 @@ pub fn liquidate_spot( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -2772,8 +2776,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", )?; @@ -2821,13 +2827,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) @@ -2878,22 +2878,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, @@ -2955,25 +2941,27 @@ pub fn liquidate_perp_pnl_for_deposit( ) }; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), + margin_context, )?; - if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_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 liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(user)?; return Ok(()); } let liquidation_id = user.enter_liquidation(slot)?; let mut margin_freed = 0_u64; + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -2984,25 +2972,27 @@ pub fn liquidate_perp_pnl_for_deposit( now, slot, OrderActionExplanation::Liquidation, + cancel_orders_market_type, + cancel_orders_market_index, None, - None, - None, + cancel_orders_is_isolated, )?; let (safest_tier_spot_liability, safest_tier_perp_liability) = - calculate_user_safest_position_tiers(user, perp_market_map, spot_market_map)?; + 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)); // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), + margin_context, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -3011,7 +3001,7 @@ pub fn liquidate_perp_pnl_for_deposit( 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()?; @@ -3042,7 +3032,7 @@ pub fn liquidate_perp_pnl_for_deposit( }); 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 {:?} & {:?}", @@ -3088,7 +3078,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, @@ -3176,12 +3166,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), )?; } @@ -3202,18 +3190,21 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; 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_isolated_position_market_index = liquidator.get_perp_position(perp_market_index)?.is_isolated().then_some(perp_market_index); + let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; validate!( liquidator_meets_initial_margin_requirement, @@ -3262,12 +3253,14 @@ 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.user_is_bankrupt(user)? && liquidation_mode.should_user_enter_bankruptcy(user)? { + liquidation_mode.enter_bankruptcy(user)?; } validate!( - user.is_bankrupt(), + liquidation_mode.user_is_bankrupt(user)?, ErrorCode::UserNotBankrupt, "user not bankrupt", )?; @@ -3314,6 +3307,7 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; + let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let MarginCalculation { margin_requirement, total_collateral, @@ -3323,7 +3317,7 @@ pub fn resolve_perp_bankruptcy( perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Maintenance), + margin_context, )?; // spot market's insurance fund draw attempt here (before social loss) @@ -3446,8 +3440,8 @@ 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)?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 6648417692..ed15ef0262 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -1,6 +1,8 @@ -use crate::{error::DriftResult, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate}, state::margin_calculation::{MarginContext, MarketIdentifier}, LIQUIDATION_PCT_PRECISION}; +use solana_program::msg; -use super::user::{MarketType, User}; +use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers}, state::margin_calculation::{MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; + +use super::{perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; pub trait LiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult; @@ -24,9 +26,25 @@ pub trait LiquidatePerpMode { 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 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<()>; } pub fn get_perp_liquidation_mode(user: &User, market_index: u16) -> Box { @@ -45,8 +63,7 @@ impl CrossMarginLiquidatePerpMode { impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) } fn user_is_being_liquidated(&self, user: &User) -> DriftResult { @@ -86,6 +103,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(is_user_bankrupt(user)) } + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { + Ok(is_user_bankrupt(user)) + } + fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { Ok(user.enter_bankruptcy()) } @@ -93,6 +114,65 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { Ok(user.exit_bankruptcy()) } + + 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(()) + } } pub struct IsolatedLiquidatePerpMode { @@ -107,9 +187,7 @@ impl IsolatedLiquidatePerpMode { impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - MarginContext::liquidation(liquidation_margin_buffer_ratio) - .isolated_position_market_index(self.market_index) - .track_market_margin_requirement(MarketIdentifier::perp(self.market_index)) + Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(self.market_index)) } fn user_is_being_liquidated(&self, user: &User) -> DriftResult { @@ -140,6 +218,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { } fn is_user_bankrupt(&self, user: &User) -> DriftResult { + user.is_isolated_position_bankrupt(self.market_index) + } + + fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { is_user_isolated_position_bankrupt(user, self.market_index) } @@ -150,4 +232,53 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { user.exit_isolated_position_bankruptcy(self.market_index) } + + 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_position_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.get_isolated_perp_position_mut(&self.market_index)?; + + update_spot_balances( + token_amount, + &SpotBalanceType::Borrow, + spot_market, + perp_position, + false, + )?; + + Ok(()) + } } \ No newline at end of file diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index e0fc445204..f32c7dd9f7 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -434,6 +434,11 @@ impl User { Ok(()) } + pub fn is_isolated_position_bankrupt(&self, perp_market_index: u16) -> DriftResult { + let perp_position = self.get_isolated_perp_position(perp_market_index)?; + Ok(perp_position.position_flag & (PositionFlag::Bankruptcy 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(()) From 8e89ef411f4efe2ace98cd141c8ee7caf25a2ef3 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 30 Jul 2025 19:32:19 -0400 Subject: [PATCH 016/159] make build work --- programs/drift/src/controller/liquidation.rs | 71 +++++++++++++------- programs/drift/src/math/bankruptcy.rs | 1 + programs/drift/src/state/liquidation_mode.rs | 2 +- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 37cf9cd0f6..e4bec91158 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -96,10 +96,10 @@ pub fn liquidate_perp( let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; let liquidation_duration = state.liquidation_duration as u128; - let liquidation_mode = get_perp_liquidation_mode(user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index); validate!( - !liquidation_mode.is_user_bankrupt(user)?, + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -150,7 +150,7 @@ pub fn liquidate_perp( liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(user)?; + let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; if !user_is_being_liquidated && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); @@ -733,10 +733,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); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index); validate!( - !liquidation_mode.is_user_bankrupt(user)?, + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -788,11 +788,11 @@ pub fn liquidate_perp_with_fill( margin_context, )?; - if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { - liquidation_mode.exit_liquidation(user)?; + } else if liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { + liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -869,7 +869,7 @@ pub fn liquidate_perp_with_fill( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - liquidation_mode.increment_free_margin(user, margin_freed); + liquidation_mode.increment_free_margin(&mut user, margin_freed); if intermediate_margin_calculation.can_exit_liquidation()? { emit!(LiquidationRecord { @@ -1109,12 +1109,12 @@ pub fn liquidate_perp_with_fill( )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); + liquidation_mode.increment_free_margin(&mut user, margin_freed_for_perp_position); if margin_calculation_after.meets_margin_requirement() { - liquidation_mode.exit_liquidation(user)?; - } else if liquidation_mode.should_user_enter_bankruptcy(user)? { - liquidation_mode.enter_bankruptcy(user)?; + 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( @@ -1410,6 +1410,7 @@ pub fn liquidate_spot( None, None, None, + true, )?; // check if user exited liquidation territory @@ -1940,6 +1941,7 @@ pub fn liquidate_spot_with_swap_begin( None, None, None, + true )?; // check if user exited liquidation territory @@ -2246,6 +2248,7 @@ pub fn liquidate_spot_with_swap_end( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + MarginContext::liquidation(liquidation_margin_buffer_ratio) )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; @@ -2512,6 +2515,7 @@ pub fn liquidate_borrow_for_perp_pnl( None, None, None, + true )?; // check if user exited liquidation territory @@ -2705,6 +2709,8 @@ pub fn liquidate_borrow_for_perp_pnl( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -2776,10 +2782,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); + let liquidation_mode = get_perp_liquidation_mode(&user, perp_market_index); validate!( - !liquidation_mode.is_user_bankrupt(user)?, + !liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserBankrupt, "user bankrupt", )?; @@ -2950,10 +2956,10 @@ pub fn liquidate_perp_pnl_for_deposit( margin_context, )?; - if !liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.meets_margin_requirement() { + if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if liquidation_mode.user_is_being_liquidated(user)? && margin_calculation.can_exit_liquidation()? { + } else if liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -3253,14 +3259,14 @@ pub fn resolve_perp_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - let liquidation_mode = get_perp_liquidation_mode(user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index); - if !liquidation_mode.user_is_bankrupt(user)? && liquidation_mode.should_user_enter_bankruptcy(user)? { + if !liquidation_mode.is_user_bankrupt(&user)? && liquidation_mode.should_user_enter_bankruptcy(&user)? { liquidation_mode.enter_bankruptcy(user)?; } validate!( - liquidation_mode.user_is_bankrupt(user)?, + liquidation_mode.is_user_bankrupt(&user)?, ErrorCode::UserNotBankrupt, "user not bankrupt", )?; @@ -3307,7 +3313,7 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; + let margin_context = liquidation_mode.get_margin_context(0)?; let MarginCalculation { margin_requirement, total_collateral, @@ -3679,11 +3685,26 @@ 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); - return Err(ErrorCode::SufficientCollateral); - } else { + if !user.is_being_liquidated() && !margin_calculation.meets_margin_requirement() { user.enter_liquidation(slot)?; } + + let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); + + for market_index in isolated_position_market_indexes { + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + perp_market_map, + spot_market_map, + oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(market_index), + )?; + + if !user.is_isolated_position_being_liquidated(market_index)? && !margin_calculation.meets_margin_requirement() { + user.enter_isolated_position_liquidation(market_index)?; + } + + } + Ok(()) } diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 6e152857af..7defdea6b3 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -1,3 +1,4 @@ +use crate::error::DriftResult; use crate::state::spot_market::SpotBalanceType; use crate::state::user::User; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index ed15ef0262..a86d5a929f 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -269,7 +269,7 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { spot_market: &mut SpotMarket, cumulative_deposit_delta: Option, ) -> DriftResult<()> { - let perp_position = user.get_isolated_perp_position_mut(&self.market_index)?; + let perp_position = user.force_get_isolated_perp_position_mut(self.market_index)?; update_spot_balances( token_amount, From 8062d60241f99350ce6cc351b9e3e7019a1f8609 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 31 Jul 2025 18:30:39 -0400 Subject: [PATCH 017/159] more updates --- programs/drift/src/controller/orders.rs | 35 ++++++++++++--- programs/drift/src/controller/pnl.rs | 8 +--- programs/drift/src/instructions/keeper.rs | 54 +++++++++++++++++------ programs/drift/src/instructions/user.rs | 29 ++++++++++-- programs/drift/src/math/liquidation.rs | 32 +++++++++++--- programs/drift/src/math/orders.rs | 9 +++- 6 files changed, 127 insertions(+), 40 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index a1cab6290c..695e2179e1 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -115,6 +115,7 @@ pub fn place_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + Some(params.market_index), )?; } @@ -1047,6 +1048,7 @@ pub fn fill_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + Some(market_index), ) { Ok(_) => {} Err(_) => { @@ -1737,6 +1739,8 @@ fn fulfill_perp_order( let user_order_position_decreasing = determine_if_user_order_is_position_decreasing(user, market_index, user_order_index)?; + let user_is_isolated_position = user.get_perp_position(market_index)?.is_isolated(); + let perp_market = perp_market_map.get_ref(&market_index)?; let limit_price = fill_mode.get_limit_price( &user.orders[user_order_index], @@ -1772,7 +1776,7 @@ fn fulfill_perp_order( let mut base_asset_amount = 0_u64; let mut quote_asset_amount = 0_u64; - let mut maker_fills: BTreeMap = BTreeMap::new(); + let mut maker_fills: BTreeMap = BTreeMap::new(); let maker_direction = user.orders[user_order_index].direction.opposite(); for fulfillment_method in fulfillment_methods.iter() { if user.orders[user_order_index].status != OrderStatus::Open { @@ -1849,6 +1853,8 @@ fn fulfill_perp_order( Some(&maker), )?; + let maker_is_isolated_position = maker.get_perp_position(market_index)?.is_isolated(); + let (fill_base_asset_amount, fill_quote_asset_amount, maker_fill_base_asset_amount) = fulfill_perp_order_with_match( market.deref_mut(), @@ -1882,6 +1888,7 @@ fn fulfill_perp_order( maker_key, maker_direction, maker_fill_base_asset_amount, + maker_is_isolated_position, )?; } @@ -1904,7 +1911,7 @@ fn fulfill_perp_order( quote_asset_amount )?; - let total_maker_fill = maker_fills.values().sum::(); + let total_maker_fill = maker_fills.values().map(|(fill, _)| fill).sum::(); validate!( total_maker_fill.unsigned_abs() <= base_asset_amount, @@ -1934,6 +1941,10 @@ fn fulfill_perp_order( context = context.margin_ratio_override(MARGIN_PRECISION); } + if user_is_isolated_position { + context = context.isolated_position_market_index(market_index); + } + let taker_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -1961,7 +1972,7 @@ fn fulfill_perp_order( } } - for (maker_key, maker_base_asset_amount_filled) in maker_fills { + for (maker_key, (maker_base_asset_amount_filled, maker_is_isolated_position)) in maker_fills { let mut maker = makers_and_referrer.get_ref_mut(&maker_key)?; let maker_stats = if maker.authority == user.authority { @@ -1992,6 +2003,10 @@ fn fulfill_perp_order( } } + if maker_is_isolated_position { + context = context.isolated_position_market_index(market_index); + } + let maker_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &maker, @@ -2060,20 +2075,21 @@ fn get_referrer<'a>( #[inline(always)] fn update_maker_fills_map( - map: &mut BTreeMap, + map: &mut BTreeMap, maker_key: &Pubkey, maker_direction: PositionDirection, fill: u64, + is_isolated_position: bool, ) -> DriftResult { let signed_fill = match maker_direction { PositionDirection::Long => fill.cast::()?, PositionDirection::Short => -fill.cast::()?, }; - if let Some(maker_filled) = map.get_mut(maker_key) { + if let Some((maker_filled, _)) = map.get_mut(maker_key) { *maker_filled = maker_filled.safe_add(signed_fill)?; } else { - map.insert(*maker_key, signed_fill); + map.insert(*maker_key, (signed_fill, is_isolated_position)); } Ok(()) @@ -2958,6 +2974,7 @@ pub fn trigger_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + Some(market_index), )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -3381,6 +3398,7 @@ pub fn place_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + None, )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -3725,6 +3743,7 @@ pub fn fill_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + None, ) { Ok(_) => {} Err(_) => { @@ -4241,7 +4260,7 @@ fn fulfill_spot_order( let mut base_asset_amount = 0_u64; let mut quote_asset_amount = 0_u64; - let mut maker_fills: BTreeMap = BTreeMap::new(); + let mut maker_fills: BTreeMap = BTreeMap::new(); let maker_direction = user.orders[user_order_index].direction.opposite(); for fulfillment_method in fulfillment_methods.iter() { if user.orders[user_order_index].status != OrderStatus::Open { @@ -4283,6 +4302,7 @@ fn fulfill_spot_order( maker_key, maker_direction, base_filled, + false, )?; } @@ -5205,6 +5225,7 @@ pub fn trigger_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, + None, )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index cff0b45ccb..5c0648c0b5 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -17,9 +17,7 @@ use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; use crate::math::casting::Cast; use crate::math::margin::{ - calculate_margin_requirement_and_total_collateral_and_liability_info, meets_maintenance_margin_requirement, meets_settle_pnl_maintenance_margin_requirement, - MarginRequirementType, }; use crate::math::position::calculate_base_asset_value_with_expiry_price; use crate::math::safe_math::SafeMath; @@ -83,11 +81,7 @@ pub fn settle_pnl( // cannot settle negative pnl this way on a user who is in liquidation territory if unrealized_pnl < 0 { - let isolated_position_market_index = if user.perp_positions[position_index].is_isolated() { - Some(market_index) - } else { - None - }; + let isolated_position_market_index = user.perp_positions[position_index].is_isolated().then_some(market_index); // may already be cached let meets_margin_requirement = match meets_margin_requirement { diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index a272f8e06e..10d06c7fd0 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,4 +1,6 @@ use std::cell::RefMut; +use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::convert::TryFrom; use anchor_lang::prelude::*; @@ -2721,35 +2723,59 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( let custom_margin_ratio_before = user.max_margin_ratio; user.max_margin_ratio = 0; + let margin_buffer= MARGIN_PRECISION / 100; // 1% buffer let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, &perp_market_map, &spot_market_map, &mut oracle_map, MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(MARGIN_PRECISION / 100), // 1% buffer + .margin_buffer(margin_buffer), )?; - user.max_margin_ratio = custom_margin_ratio_before; + let meets_cross_margin_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); + + let isolated_position_market_indexes = user.perp_positions.iter().filter(|p| p.is_isolated()).map(|p| p.market_index).collect::>(); + + let mut isolated_position_margin_calcs : BTreeMap = BTreeMap::new(); + + for market_index in isolated_position_market_indexes { + let isolated_position_margin_calc = 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(margin_buffer) + .isolated_position_market_index(market_index), + )?; + + isolated_position_margin_calcs.insert(market_index, isolated_position_margin_calc.meets_margin_requirement_with_buffer()); + } - if margin_calc.num_perp_liabilities > 0 { - let mut requires_invariant_check = false; + user.max_margin_ratio = custom_margin_ratio_before; + if margin_calc.num_perp_liabilities > 0 || isolated_position_margin_calcs.len() > 0 { 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 + if position.is_isolated() { + let meets_isolated_position_margin_calc = isolated_position_margin_calcs.get(&position.market_index).unwrap(); + validate!( + *meets_isolated_position_margin_calc, + ErrorCode::DefaultError, + "User does not meet margin requirement with buffer for isolated position (market index = {})", + position.market_index + )?; + } else { + validate!( + meets_cross_margin_margin_calc, + 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 diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index ec68ed4306..a12a12a290 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1731,13 +1731,17 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let ( from_existing_quote_entry_amount, from_existing_base_asset_amount, + from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, + to_user_is_isolated_position, ) = { let mut market = perp_market_map.get_ref_mut(&market_index)?; let from_user_position = from_user.force_get_perp_position_mut(market_index)?; + let from_user_is_isolated_position = from_user_position.is_isolated(); + let (from_existing_quote_entry_amount, from_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1749,6 +1753,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let to_user_position = to_user.force_get_perp_position_mut(market_index)?; + let to_user_is_isolated_position = to_user_position.is_isolated(); + let (to_existing_quote_entry_amount, to_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1764,19 +1770,27 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( ( from_existing_quote_entry_amount, from_existing_base_asset_amount, + from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, + to_user_is_isolated_position, ) }; + let mut from_user_margin_context = MarginContext::standard(MarginRequirementType::Maintenance) + .fuel_perp_delta(market_index, transfer_amount); + + if from_user_is_isolated_position { + from_user_margin_context = from_user_margin_context.isolated_position_market_index(market_index); + } + 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!( @@ -1785,14 +1799,20 @@ 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); + + if to_user_is_isolated_position { + to_user_margin_context = to_user_margin_context.isolated_position_market_index(market_index); + } + 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!( @@ -3813,6 +3833,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, ctx.accounts.state.liquidation_margin_buffer_ratio, + None, )?; let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 4035719290..e035c019bf 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -224,18 +224,36 @@ pub fn validate_user_not_being_liquidated( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, + perp_market_index: Option, ) -> DriftResult { if !user.is_being_liquidated() { return Ok(()); } - let is_still_being_liquidated = is_user_being_liquidated( - user, - market_map, - spot_market_map, - oracle_map, - liquidation_margin_buffer_ratio, - )?; + let is_isolated_perp_market = if let Some(perp_market_index) = perp_market_index { + user.force_get_perp_position_mut(perp_market_index)?.is_isolated() + } else { + false + }; + + let is_still_being_liquidated = if is_isolated_perp_market { + is_isolated_position_being_liquidated( + user, + market_map, + spot_market_map, + oracle_map, + perp_market_index.unwrap(), + liquidation_margin_buffer_ratio, + )? + } else { + is_user_being_liquidated( + user, + market_map, + spot_market_map, + oracle_map, + liquidation_margin_buffer_ratio, + )? + }; if is_still_being_liquidated { return Err(ErrorCode::UserIsBeingLiquidated); diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index ec146bd1f7..c608e922a5 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -845,6 +845,13 @@ pub fn calculate_max_perp_order_size( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, ) -> DriftResult { + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + let mut margin_context = MarginContext::standard(MarginRequirementType::Initial).strict(true); + + if is_isolated_position { + margin_context = margin_context.isolated_position_market_index(user.perp_positions[position_index].market_index); + } + // calculate initial margin requirement let MarginCalculation { margin_requirement, @@ -855,7 +862,7 @@ pub fn calculate_max_perp_order_size( 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; From 991dda9e5f0ce2c80c3ff32536c202dcd21d3486 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 4 Aug 2025 17:52:04 -0400 Subject: [PATCH 018/159] always calc isolated pos --- programs/drift/src/controller/liquidation.rs | 12 +- programs/drift/src/math/margin.rs | 218 ++++-------------- .../drift/src/state/margin_calculation.rs | 96 +++++++- programs/drift/src/state/user.rs | 4 +- 4 files changed, 141 insertions(+), 189 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index e4bec91158..07e756f7a1 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -271,7 +271,7 @@ pub fn liquidate_perp( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if user.perp_positions[position_index].base_asset_amount == 0 { @@ -898,7 +898,7 @@ pub fn liquidate_perp_with_fill( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if user.perp_positions[position_index].base_asset_amount == 0 { @@ -1466,7 +1466,7 @@ pub fn liquidate_spot( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; let margin_shortage = intermediate_margin_calculation.margin_shortage()?; @@ -1997,7 +1997,7 @@ pub fn liquidate_spot_with_swap_begin( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; let margin_shortage = intermediate_margin_calculation.margin_shortage()?; @@ -2569,7 +2569,7 @@ pub fn liquidate_borrow_for_perp_pnl( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; let margin_shortage = intermediate_margin_calculation.margin_shortage()?; @@ -3053,7 +3053,7 @@ pub fn liquidate_perp_pnl_for_deposit( intermediate_margin_calculation } else { - margin_calculation + margin_calculation.clone() }; if is_contract_tier_violation { diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index aa81af0add..d2f7ef16d9 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -6,6 +6,7 @@ use crate::math::constants::{ }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; +use crate::state::margin_calculation::IsolatedPositionMarginCalculation; use crate::{validate, PRICE_PRECISION_I128}; use crate::{validation, PRICE_PRECISION_I64}; @@ -236,10 +237,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_map: &mut OracleMap, context: MarginContext, ) -> DriftResult { - if context.isolated_position_market_index.is_some() { - return calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position(user, perp_market_map, spot_market_map, oracle_map, context); - } - let mut calculation = MarginCalculation::new(context); let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { @@ -501,10 +498,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( continue; } - if market_position.is_isolated() { - continue; - } - let market = &perp_market_map.get_ref(&market_position.market_index)?; validate!( @@ -570,17 +563,45 @@ 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, + )?; + + let quote_token_value = get_strict_token_value( + quote_token_amount.cast::()?, + quote_spot_market.decimals, + &strict_quote_price, + )?; - if calculation.track_open_orders_fraction() { - calculation.add_open_orders_margin_requirement(open_order_margin_requirement)?; - } + calculation.add_isolated_position_margin_calculation( + market.market_index, + quote_token_value, + weighted_pnl, + worst_case_liability_value, + perp_margin_requirement, + )?; - calculation.add_total_collateral(weighted_pnl)?; + #[cfg(feature = "drift-rs")] + calculation.add_spot_asset_value(quote_token_value)?; + } else { + calculation.add_margin_requirement( + perp_margin_requirement, + worst_case_liability_value, + MarketIdentifier::perp(market.market_index), + )?; + + if calculation.track_open_orders_fraction() { + calculation.add_open_orders_margin_requirement(open_order_margin_requirement)?; + } + + calculation.add_total_collateral(weighted_pnl)?; + } #[cfg(feature = "drift-rs")] calculation.add_perp_liability_value(worst_case_liability_value)?; @@ -645,168 +666,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( Ok(calculation) } -pub fn calculate_margin_requirement_and_total_collateral_and_liability_info_for_isolated_position( - user: &User, - perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, - oracle_map: &mut OracleMap, - context: MarginContext, -) -> DriftResult { - let mut calculation = MarginCalculation::new(context); - - let mut user_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { - user.max_margin_ratio - } else { - 0_u32 - }; - - if let Some(margin_ratio_override) = context.margin_ratio_override { - user_custom_margin_ratio = margin_ratio_override.max(user_custom_margin_ratio); - } - - let user_pool_id = user.pool_id; - let user_high_leverage_mode = user.is_high_leverage_mode(); - - let isolated_position_market_index = context.isolated_position_market_index.unwrap(); - - let perp_position = user.get_perp_position(isolated_position_market_index)?; - - let perp_market = perp_market_map.get_ref(&isolated_position_market_index)?; - - validate!( - user_pool_id == perp_market.pool_id, - ErrorCode::InvalidPoolId, - "user pool id ({}) == perp market pool id ({})", - user_pool_id, - perp_market.pool_id, - )?; - - let quote_spot_market = spot_market_map.get_ref(&perp_market.quote_spot_market_index)?; - - validate!( - user_pool_id == quote_spot_market.pool_id, - ErrorCode::InvalidPoolId, - "user pool id ({}) == quote spot market pool id ({})", - user_pool_id, - quote_spot_market.pool_id, - )?; - - let (quote_oracle_price_data, quote_oracle_validity) = oracle_map.get_price_data_and_validity( - MarketType::Spot, - quote_spot_market.market_index, - "e_spot_market.oracle_id(), - quote_spot_market - .historical_oracle_data - .last_oracle_price_twap, - quote_spot_market.get_max_confidence_interval_multiplier()?, - 0, - )?; - - let quote_oracle_valid = - is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; - - let quote_strict_oracle_price = StrictOraclePrice::new( - quote_oracle_price_data.price, - quote_spot_market - .historical_oracle_data - .last_oracle_price_twap_5min, - calculation.context.strict, - ); - quote_strict_oracle_price.validate()?; - - let quote_token_amount = get_token_amount( - perp_position - .isolated_position_scaled_balance - .cast::()?, - "e_spot_market, - &SpotBalanceType::Deposit, - )?; - - let quote_token_value = get_strict_token_value( - quote_token_amount.cast::()?, - quote_spot_market.decimals, - "e_strict_oracle_price, - )?; - - calculation.add_total_collateral(quote_token_value)?; - - calculation.update_all_deposit_oracles_valid(quote_oracle_valid); - - #[cfg(feature = "drift-rs")] - calculation.add_spot_asset_value(quote_token_value)?; - - let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( - MarketType::Perp, - isolated_position_market_index, - &perp_market.oracle_id(), - perp_market - .amm - .historical_oracle_data - .last_oracle_price_twap, - perp_market.get_max_confidence_interval_multiplier()?, - 0, - )?; - - let ( - perp_margin_requirement, - weighted_pnl, - worst_case_liability_value, - open_order_margin_requirement, - base_asset_value, - ) = calculate_perp_position_value_and_pnl( - &perp_position, - &perp_market, - oracle_price_data, - "e_strict_oracle_price, - context.margin_type, - user_custom_margin_ratio, - user_high_leverage_mode, - calculation.track_open_orders_fraction(), - )?; - - calculation.add_margin_requirement( - perp_margin_requirement, - worst_case_liability_value, - MarketIdentifier::perp(isolated_position_market_index), - )?; - - calculation.add_total_collateral(weighted_pnl)?; - - #[cfg(feature = "drift-rs")] - calculation.add_perp_liability_value(worst_case_liability_value)?; - #[cfg(feature = "drift-rs")] - calculation.add_perp_pnl(weighted_pnl)?; - - let has_perp_liability = perp_position.base_asset_amount != 0 - || perp_position.quote_asset_amount < 0 - || perp_position.has_open_order(); - - if has_perp_liability { - calculation.add_perp_liability()?; - calculation.update_with_perp_isolated_liability( - perp_market.contract_tier == ContractTier::Isolated, - ); - } - - if has_perp_liability || calculation.context.margin_type != MarginRequirementType::Initial { - calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( - quote_oracle_validity, - Some(DriftAction::MarginCalc), - )?); - calculation.update_all_liability_oracles_valid(is_oracle_valid_for_action( - oracle_validity, - Some(DriftAction::MarginCalc), - )?); - } - - calculation.validate_num_spot_liabilities()?; - - Ok(calculation) -} - 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!( @@ -880,7 +742,7 @@ pub fn meets_place_order_margin_requirement( return Err(ErrorCode::InsufficientCollateral); } - validate_any_isolated_tier_requirements(user, calculation)?; + validate_any_isolated_tier_requirements(user, &calculation)?; Ok(()) } diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 1756a5a7b8..038e87cdf5 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::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; @@ -183,7 +185,7 @@ impl MarginContext { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct MarginCalculation { pub context: MarginContext, pub total_collateral: i128, @@ -196,6 +198,7 @@ pub struct MarginCalculation { margin_requirement_plus_buffer: u128, #[cfg(test)] pub margin_requirement_plus_buffer: u128, + pub isolated_position_margin_calculation: BTreeMap, pub num_spot_liabilities: u8, pub num_perp_liabilities: u8, pub all_deposit_oracles_valid: bool, @@ -213,6 +216,29 @@ pub struct MarginCalculation { pub fuel_positions: u32, } +#[derive(Clone, Copy, Debug, Default)] +pub struct IsolatedPositionMarginCalculation { + pub margin_requirement: u128, + pub total_collateral: i128, + pub total_collateral_buffer: i128, + pub margin_requirement_plus_buffer: u128, +} + +impl IsolatedPositionMarginCalculation { + + 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 + } +} + impl MarginCalculation { pub fn new(context: MarginContext) -> Self { Self { @@ -221,6 +247,7 @@ impl MarginCalculation { total_collateral_buffer: 0, margin_requirement: 0, margin_requirement_plus_buffer: 0, + isolated_position_margin_calculation: BTreeMap::new(), num_spot_liabilities: 0, num_perp_liabilities: 0, all_deposit_oracles_valid: true, @@ -280,6 +307,41 @@ impl MarginCalculation { Ok(()) } + pub fn add_isolated_position_margin_calculation(&mut self, market_index: u16, deposit_value: i128, pnl: i128, liability_value: u128, margin_requirement: u128) -> DriftResult { + let total_collateral = deposit_value.cast::()?.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_position_margin_calculation = IsolatedPositionMarginCalculation { + margin_requirement, + total_collateral, + total_collateral_buffer, + margin_requirement_plus_buffer, + }; + + self.isolated_position_margin_calculation.insert(market_index, isolated_position_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_plus_buffer)?; + } + } + + Ok(()) + } + pub fn add_open_orders_margin_requirement(&mut self, margin_requirement: u128) -> DriftResult { self.open_orders_margin_requirement = self .open_orders_margin_requirement @@ -365,11 +427,39 @@ impl MarginCalculation { } pub fn meets_margin_requirement(&self) -> bool { - self.total_collateral >= self.margin_requirement as i128 + let cross_margin_meets_margin_requirement = self.total_collateral >= self.margin_requirement as i128; + + if !cross_margin_meets_margin_requirement { + msg!("cross margin margin calculation doesnt meet margin requirement"); + return false; + } + + for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + if !isolated_position_margin_calculation.meets_margin_requirement() { + msg!("isolated position margin calculation for market {} does not meet margin requirement", market_index); + 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.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128; + + if !cross_margin_meets_margin_requirement { + msg!("cross margin margin calculation doesnt meet margin requirement with buffer"); + return false; + } + + for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { + msg!("isolated position margin calculation for market {} does not meet margin requirement with buffer", market_index); + return false; + } + } + + true } pub fn positions_meets_margin_requirement(&self) -> DriftResult { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index f32c7dd9f7..8c4f70a82a 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -609,7 +609,7 @@ impl User { )?; } - validate_any_isolated_tier_requirements(self, calculation)?; + validate_any_isolated_tier_requirements(self, &calculation)?; validate!( calculation.meets_margin_requirement(), @@ -670,7 +670,7 @@ impl User { )?; } - validate_any_isolated_tier_requirements(self, calculation)?; + validate_any_isolated_tier_requirements(self, &calculation)?; validate!( calculation.meets_margin_requirement(), From c627c1e8c58ebb31a3ca81ffd4c559f6a68d57c3 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 4 Aug 2025 18:29:09 -0400 Subject: [PATCH 019/159] rm isolated position market index logic --- programs/drift/src/controller/liquidation.rs | 10 ++---- programs/drift/src/controller/orders.rs | 14 ++------ programs/drift/src/controller/pnl.rs | 5 +-- programs/drift/src/controller/pnl/tests.rs | 1 - programs/drift/src/instructions/keeper.rs | 27 ++++----------- programs/drift/src/instructions/user.rs | 2 +- programs/drift/src/math/margin.rs | 34 +++---------------- .../drift/src/state/margin_calculation.rs | 11 +++--- 8 files changed, 26 insertions(+), 78 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 07e756f7a1..86d2d4877d 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -559,10 +559,8 @@ pub fn liquidate_perp( liquidation_mode.enter_bankruptcy(user); } - let liquidator_isolated_position_market_index = liquidator.get_perp_position(market_index)?.is_isolated().then_some(market_index); - let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; validate!( liquidator_meets_initial_margin_requirement, @@ -2722,7 +2720,7 @@ pub fn liquidate_borrow_for_perp_pnl( } let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, None)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; validate!( liquidator_meets_initial_margin_requirement, @@ -3207,10 +3205,8 @@ pub fn liquidate_perp_pnl_for_deposit( liquidation_mode.enter_bankruptcy(user)?; } - let liquidator_isolated_position_market_index = liquidator.get_perp_position(perp_market_index)?.is_isolated().then_some(perp_market_index); - let liquidator_meets_initial_margin_requirement = - meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map, liquidator_isolated_position_market_index)?; + meets_initial_margin_requirement(liquidator, perp_market_map, spot_market_map, oracle_map)?; validate!( liquidator_meets_initial_margin_requirement, diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 695e2179e1..aeeb19c0ed 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -357,21 +357,14 @@ pub fn place_perp_order( options.update_risk_increasing(risk_increasing); - let isolated_position_market_index = if user.perp_positions[position_index].is_isolated() { - Some(market_index) - } else { - None - }; - // when orders are placed in bulk, only need to check margin on last place - if (options.enforce_margin_check || isolated_position_market_index.is_some()) && !options.is_liquidation() { + if options.enforce_margin_check && !options.is_liquidation() { meets_place_order_margin_requirement( user, perp_market_map, spot_market_map, oracle_map, options.risk_increasing, - isolated_position_market_index, )?; } @@ -3107,7 +3100,7 @@ pub fn trigger_order( }; let meets_initial_margin_requirement = - meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, isolated_position_market_index)?; + meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; if !meets_initial_margin_requirement { cancel_order( @@ -3611,7 +3604,6 @@ pub fn place_spot_order( spot_market_map, oracle_map, options.risk_increasing, - None, )?; } @@ -5375,7 +5367,7 @@ pub fn trigger_spot_order( // If order is risk increasing and user is below initial margin, cancel it if is_risk_increasing && !user.orders[order_index].reduce_only { let meets_initial_margin_requirement = - meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, None)?; + meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; if !meets_initial_margin_requirement { cancel_order( diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 5c0648c0b5..a5a59a5f1b 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -81,18 +81,15 @@ pub fn settle_pnl( // cannot settle negative pnl this way on a user who is in liquidation territory if unrealized_pnl < 0 { - let isolated_position_market_index = user.perp_positions[position_index].is_isolated().then_some(market_index); - // may already be cached let meets_margin_requirement = match meets_margin_requirement { - Some(meets_margin_requirement) if !isolated_position_market_index.is_some() => meets_margin_requirement, + Some(meets_margin_requirement) => meets_margin_requirement, // TODO check margin for isolate position _ => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, oracle_map, - isolated_position_market_index, )?, }; diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index ee6dd872b7..8d755bcfff 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -410,7 +410,6 @@ pub fn user_does_not_meet_strict_maintenance_requirement() { &market_map, &spot_market_map, &mut oracle_map, - None, ) .unwrap(); diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 10d06c7fd0..34cbab1dfa 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -991,25 +991,12 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - let mut try_cache_margin_requirement = false; - for market_index in market_indexes.iter() { - if !user.get_perp_position(*market_index)?.is_isolated() { - try_cache_margin_requirement = true; - break; - } - } - - let meets_margin_requirement = if try_cache_margin_requirement { - Some(meets_settle_pnl_maintenance_margin_requirement( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - None, - )?) - } else { - None - }; + let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; for market_index in market_indexes.iter() { let market_in_settlement = @@ -1050,7 +1037,7 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( &mut oracle_map, &clock, state, - meets_margin_requirement, + Some(meets_margin_requirement), mode, ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a12a12a290..236823e433 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3536,7 +3536,7 @@ pub fn handle_update_user_pool_id<'c: 'info, 'info>( user.pool_id = pool_id; // will throw if user has deposits/positions in other pools - meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map, None)?; + meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; Ok(()) } diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index d2f7ef16d9..94b42d8589 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -6,7 +6,6 @@ use crate::math::constants::{ }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; -use crate::state::margin_calculation::IsolatedPositionMarginCalculation; use crate::{validate, PRICE_PRECISION_I128}; use crate::{validation, PRICE_PRECISION_I64}; @@ -711,34 +710,23 @@ pub fn meets_place_order_margin_requirement( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, risk_increasing: bool, - isolated_position_market_index: Option, ) -> DriftResult { let margin_type = if risk_increasing { MarginRequirementType::Initial } else { MarginRequirementType::Maintenance }; - let context = MarginContext::standard(margin_type).strict(true); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } 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 - ); + calculation.print_margin_calculations(); return Err(ErrorCode::InsufficientCollateral); } @@ -752,20 +740,13 @@ pub fn meets_initial_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - isolated_position_market_index: Option, ) -> DriftResult { - let context = MarginContext::standard(MarginRequirementType::Initial); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } - calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(MarginRequirementType::Initial), ) .map(|calc| calc.meets_margin_requirement()) } @@ -775,20 +756,13 @@ pub fn meets_settle_pnl_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - isolated_position_market_index: Option, ) -> DriftResult { - let context = MarginContext::standard(MarginRequirementType::Maintenance).strict(true); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } - calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(MarginRequirementType::Maintenance).strict(true), ) .map(|calc| calc.meets_margin_requirement()) } diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 038e87cdf5..f9b6e9f2fa 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -430,13 +430,11 @@ impl MarginCalculation { let cross_margin_meets_margin_requirement = self.total_collateral >= self.margin_requirement as i128; if !cross_margin_meets_margin_requirement { - msg!("cross margin margin calculation doesnt meet margin requirement"); return false; } for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement() { - msg!("isolated position margin calculation for market {} does not meet margin requirement", market_index); return false; } } @@ -448,13 +446,11 @@ impl MarginCalculation { let cross_margin_meets_margin_requirement = self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128; if !cross_margin_meets_margin_requirement { - msg!("cross margin margin calculation doesnt meet margin requirement with buffer"); return false; } for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { - msg!("isolated position margin calculation for market {} does not meet margin requirement with buffer", market_index); return false; } } @@ -462,6 +458,13 @@ impl MarginCalculation { true } + pub fn print_margin_calculations(&self) { + msg!("cross_margin margin_requirement={}, total_collateral={}", self.margin_requirement, self.total_collateral); + for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + msg!("isolated_position for market {}: margin_requirement={}, total_collateral={}", market_index, isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral); + } + } + pub fn positions_meets_margin_requirement(&self) -> DriftResult { Ok(self.total_collateral >= self From a00f3a98ab4001ad10b9c9d4c30ce8d21831edee Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 11:24:15 -0400 Subject: [PATCH 020/159] moar --- programs/drift/src/controller/orders.rs | 29 +++++-------------- programs/drift/src/controller/pnl.rs | 8 +---- programs/drift/src/controller/pnl/tests.rs | 2 +- programs/drift/src/instructions/user.rs | 29 ++++--------------- programs/drift/src/math/margin.rs | 9 +----- .../drift/src/state/margin_calculation.rs | 1 + 6 files changed, 17 insertions(+), 61 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index aeeb19c0ed..467b27ec16 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1732,8 +1732,6 @@ fn fulfill_perp_order( let user_order_position_decreasing = determine_if_user_order_is_position_decreasing(user, market_index, user_order_index)?; - let user_is_isolated_position = user.get_perp_position(market_index)?.is_isolated(); - let perp_market = perp_market_map.get_ref(&market_index)?; let limit_price = fill_mode.get_limit_price( &user.orders[user_order_index], @@ -1769,7 +1767,7 @@ fn fulfill_perp_order( let mut base_asset_amount = 0_u64; let mut quote_asset_amount = 0_u64; - let mut maker_fills: BTreeMap = BTreeMap::new(); + let mut maker_fills: BTreeMap = BTreeMap::new(); let maker_direction = user.orders[user_order_index].direction.opposite(); for fulfillment_method in fulfillment_methods.iter() { if user.orders[user_order_index].status != OrderStatus::Open { @@ -1846,8 +1844,6 @@ fn fulfill_perp_order( Some(&maker), )?; - let maker_is_isolated_position = maker.get_perp_position(market_index)?.is_isolated(); - let (fill_base_asset_amount, fill_quote_asset_amount, maker_fill_base_asset_amount) = fulfill_perp_order_with_match( market.deref_mut(), @@ -1881,7 +1877,6 @@ fn fulfill_perp_order( maker_key, maker_direction, maker_fill_base_asset_amount, - maker_is_isolated_position, )?; } @@ -1904,7 +1899,7 @@ fn fulfill_perp_order( quote_asset_amount )?; - let total_maker_fill = maker_fills.values().map(|(fill, _)| fill).sum::(); + let total_maker_fill = maker_fills.values().sum::(); validate!( total_maker_fill.unsigned_abs() <= base_asset_amount, @@ -1934,10 +1929,6 @@ fn fulfill_perp_order( context = context.margin_ratio_override(MARGIN_PRECISION); } - if user_is_isolated_position { - context = context.isolated_position_market_index(market_index); - } - let taker_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -1965,7 +1956,7 @@ fn fulfill_perp_order( } } - for (maker_key, (maker_base_asset_amount_filled, maker_is_isolated_position)) 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 { @@ -1996,10 +1987,6 @@ fn fulfill_perp_order( } } - if maker_is_isolated_position { - context = context.isolated_position_market_index(market_index); - } - let maker_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &maker, @@ -2068,21 +2055,20 @@ fn get_referrer<'a>( #[inline(always)] fn update_maker_fills_map( - map: &mut BTreeMap, + map: &mut BTreeMap, maker_key: &Pubkey, maker_direction: PositionDirection, fill: u64, - is_isolated_position: bool, ) -> DriftResult { let signed_fill = match maker_direction { PositionDirection::Long => fill.cast::()?, PositionDirection::Short => -fill.cast::()?, }; - if let Some((maker_filled, _)) = map.get_mut(maker_key) { + if let Some(maker_filled) = map.get_mut(maker_key) { *maker_filled = maker_filled.safe_add(signed_fill)?; } else { - map.insert(*maker_key, (signed_fill, is_isolated_position)); + map.insert(*maker_key, signed_fill); } Ok(()) @@ -4252,7 +4238,7 @@ fn fulfill_spot_order( let mut base_asset_amount = 0_u64; let mut quote_asset_amount = 0_u64; - let mut maker_fills: BTreeMap = BTreeMap::new(); + let mut maker_fills: BTreeMap = BTreeMap::new(); let maker_direction = user.orders[user_order_index].direction.opposite(); for fulfillment_method in fulfillment_methods.iter() { if user.orders[user_order_index].status != OrderStatus::Open { @@ -4294,7 +4280,6 @@ fn fulfill_spot_order( maker_key, maker_direction, base_filled, - false, )?; } diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index a5a59a5f1b..65d6364be3 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -349,14 +349,8 @@ pub fn settle_expired_position( ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; - let isolated_position_market_index = if user.get_perp_position(perp_market_index)?.is_isolated() { - Some(perp_market_index) - } else { - None - }; - // cannot settle pnl this way on a user who is in liquidation territory - if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map, isolated_position_market_index)?) + if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) { return Err(ErrorCode::InsufficientCollateralForSettlingPNL); } diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index 8d755bcfff..4a35df4e49 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -400,7 +400,7 @@ pub fn user_does_not_meet_strict_maintenance_requirement() { assert_eq!(result, Err(ErrorCode::InsufficientCollateralForSettlingPNL)); let meets_maintenance = - meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map, None) + meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map) .unwrap(); assert_eq!(meets_maintenance, true); diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 236823e433..6755b1fee0 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3762,29 +3762,12 @@ pub fn handle_enable_user_high_leverage_mode<'c: 'info, 'info>( "user already in high leverage mode" )?; - let has_non_isolated_position = user.perp_positions.iter().any(|position| !position.is_isolated()); - - if has_non_isolated_position { - meets_maintenance_margin_requirement( - &user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - None, - )?; - } - - let isolated_position_market_indexes = user.perp_positions.iter().filter(|position| position.is_isolated()).map(|position| position.market_index).collect::>(); - - for market_index in isolated_position_market_indexes.iter() { - meets_maintenance_margin_requirement( - &user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - Some(*market_index), - )?; - } + meets_maintenance_margin_requirement( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + )?; let mut config = load_mut!(ctx.accounts.high_leverage_mode_config)?; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 94b42d8589..65dfe7a7a2 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -772,20 +772,13 @@ pub fn meets_maintenance_margin_requirement( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - isolated_position_market_index: Option, ) -> DriftResult { - let context = MarginContext::standard(MarginRequirementType::Maintenance); - - if let Some(isolated_position_market_index) = isolated_position_market_index { - let context = context.isolated_position_market_index(isolated_position_market_index); - } - calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - context, + MarginContext::standard(MarginRequirementType::Maintenance), ) .map(|calc| calc.meets_margin_requirement()) } diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index f9b6e9f2fa..aac4dccd7f 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -510,6 +510,7 @@ impl MarginCalculation { .safe_div(self.margin_requirement) } + // todo check every where this is used pub fn get_free_collateral(&self) -> DriftResult { self.total_collateral .safe_sub(self.margin_requirement.cast::()?)? From d435dad769e1aa6a9c7642e0123ffc5673ae66a8 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 12:46:49 -0400 Subject: [PATCH 021/159] program: rm the isolated position market index --- programs/drift/src/controller/liquidation.rs | 25 +++++------ programs/drift/src/instructions/keeper.rs | 42 ++++--------------- programs/drift/src/instructions/user.rs | 19 --------- programs/drift/src/math/liquidation.rs | 3 +- programs/drift/src/math/orders.rs | 6 +-- programs/drift/src/state/liquidation_mode.rs | 2 +- .../drift/src/state/margin_calculation.rs | 8 ---- programs/drift/src/state/user.rs | 9 +--- 8 files changed, 25 insertions(+), 89 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 86d2d4877d..5a383254e7 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3681,26 +3681,27 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; + // todo handle this if !user.is_being_liquidated() && !margin_calculation.meets_margin_requirement() { user.enter_liquidation(slot)?; } let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); - for market_index in isolated_position_market_indexes { - let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( - user, - perp_market_map, - spot_market_map, - oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(market_index), - )?; + // for market_index in isolated_position_market_indexes { + // let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + // user, + // perp_market_map, + // spot_market_map, + // oracle_map, + // MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(market_index), + // )?; - if !user.is_isolated_position_being_liquidated(market_index)? && !margin_calculation.meets_margin_requirement() { - user.enter_isolated_position_liquidation(market_index)?; - } + // if !user.is_isolated_position_being_liquidated(market_index)? && !margin_calculation.meets_margin_requirement() { + // user.enter_isolated_position_liquidation(market_index)?; + // } - } + // } Ok(()) } diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 34cbab1dfa..6efb3d7fdc 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2720,47 +2720,19 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( .margin_buffer(margin_buffer), )?; - let meets_cross_margin_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); - - let isolated_position_market_indexes = user.perp_positions.iter().filter(|p| p.is_isolated()).map(|p| p.market_index).collect::>(); - - let mut isolated_position_margin_calcs : BTreeMap = BTreeMap::new(); - - for market_index in isolated_position_market_indexes { - let isolated_position_margin_calc = 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(margin_buffer) - .isolated_position_market_index(market_index), - )?; - - isolated_position_margin_calcs.insert(market_index, isolated_position_margin_calc.meets_margin_requirement_with_buffer()); - } + let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); user.max_margin_ratio = custom_margin_ratio_before; - if margin_calc.num_perp_liabilities > 0 || isolated_position_margin_calcs.len() > 0 { + if margin_calc.num_perp_liabilities > 0 { 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() { - if position.is_isolated() { - let meets_isolated_position_margin_calc = isolated_position_margin_calcs.get(&position.market_index).unwrap(); - validate!( - *meets_isolated_position_margin_calc, - ErrorCode::DefaultError, - "User does not meet margin requirement with buffer for isolated position (market index = {})", - position.market_index - )?; - } else { - validate!( - meets_cross_margin_margin_calc, - ErrorCode::DefaultError, - "User does not meet margin requirement with buffer" - )?; - } + validate!( + meets_margin_calc, + ErrorCode::DefaultError, + "User does not meet margin requirement with buffer" + )?; } } } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 6755b1fee0..c4dbe8eaa2 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -787,7 +787,6 @@ pub fn handle_withdraw<'c: 'info, 'info>( amount as u128, &mut user_stats, now, - None, )?; validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; @@ -962,7 +961,6 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( amount as u128, user_stats, now, - None, )?; validate_spot_margin_trading( @@ -1731,17 +1729,13 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let ( from_existing_quote_entry_amount, from_existing_base_asset_amount, - from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, - to_user_is_isolated_position, ) = { let mut market = perp_market_map.get_ref_mut(&market_index)?; let from_user_position = from_user.force_get_perp_position_mut(market_index)?; - let from_user_is_isolated_position = from_user_position.is_isolated(); - let (from_existing_quote_entry_amount, from_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1753,8 +1747,6 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let to_user_position = to_user.force_get_perp_position_mut(market_index)?; - let to_user_is_isolated_position = to_user_position.is_isolated(); - let (to_existing_quote_entry_amount, to_existing_base_asset_amount) = calculate_existing_position_fields_for_order_action( transfer_amount_abs, @@ -1770,20 +1762,14 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( ( from_existing_quote_entry_amount, from_existing_base_asset_amount, - from_user_is_isolated_position, to_existing_quote_entry_amount, to_existing_base_asset_amount, - to_user_is_isolated_position, ) }; let mut from_user_margin_context = MarginContext::standard(MarginRequirementType::Maintenance) .fuel_perp_delta(market_index, transfer_amount); - if from_user_is_isolated_position { - from_user_margin_context = from_user_margin_context.isolated_position_market_index(market_index); - } - let from_user_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &from_user, @@ -1802,10 +1788,6 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let mut to_user_margin_context = MarginContext::standard(MarginRequirementType::Initial) .fuel_perp_delta(market_index, -transfer_amount); - if to_user_is_isolated_position { - to_user_margin_context = to_user_margin_context.isolated_position_market_index(market_index); - } - let to_user_margin_requirement = calculate_margin_requirement_and_total_collateral_and_liability_info( &to_user, @@ -2185,7 +2167,6 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( amount as u128, user_stats, now, - None, )?; validate_spot_margin_trading( diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index e035c019bf..cf425bade7 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -264,6 +264,7 @@ pub fn validate_user_not_being_liquidated( Ok(()) } +// todo check if this is corrects pub fn is_isolated_position_being_liquidated( user: &User, market_map: &PerpMarketMap, @@ -277,7 +278,7 @@ pub fn is_isolated_position_being_liquidated( market_map, spot_market_map, oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(perp_market_index), + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index c608e922a5..879bc4e9fe 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -847,11 +847,6 @@ pub fn calculate_max_perp_order_size( ) -> DriftResult { let is_isolated_position = user.perp_positions[position_index].is_isolated(); let mut margin_context = MarginContext::standard(MarginRequirementType::Initial).strict(true); - - if is_isolated_position { - margin_context = margin_context.isolated_position_market_index(user.perp_positions[position_index].market_index); - } - // calculate initial margin requirement let MarginCalculation { margin_requirement, @@ -868,6 +863,7 @@ pub fn calculate_max_perp_order_size( let user_custom_margin_ratio = user.max_margin_ratio; let user_high_leverage_mode = user.is_high_leverage_mode(); + // todo check if this is correct let free_collateral_before = total_collateral.safe_sub(margin_requirement.cast()?)?; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index a86d5a929f..edf8b99c7c 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -187,7 +187,7 @@ impl IsolatedLiquidatePerpMode { impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(self.market_index)) + Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) } fn user_is_being_liquidated(&self, user: &User) -> DriftResult { diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index aac4dccd7f..874b6d4e45 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -37,7 +37,6 @@ pub struct MarginContext { pub fuel_perp_delta: Option<(u16, i64)>, pub fuel_spot_deltas: [(u16, i128); 2], pub margin_ratio_override: Option, - pub isolated_position_market_index: Option, } #[derive(PartialEq, Eq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] @@ -77,7 +76,6 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, - isolated_position_market_index: None, } } @@ -156,7 +154,6 @@ impl MarginContext { fuel_perp_delta: None, fuel_spot_deltas: [(0, 0); 2], margin_ratio_override: None, - isolated_position_market_index: None, } } @@ -178,11 +175,6 @@ impl MarginContext { } Ok(self) } - - pub fn isolated_position_market_index(mut self, market_index: u16) -> Self { - self.isolated_position_market_index = Some(market_index); - self - } } #[derive(Clone, Debug)] diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 8c4f70a82a..36dd5f7aa3 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -640,7 +640,6 @@ impl User { withdraw_amount: u128, user_stats: &mut UserStats, now: i64, - isolated_perp_position_market_index: Option, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; let context = MarginContext::standard(margin_requirement_type) @@ -649,11 +648,6 @@ impl User { .fuel_spot_delta(withdraw_market_index, withdraw_amount.cast::()?) .fuel_numerator(self, now); - // TODO check if this is correct - if let Some(isolated_perp_position_market_index) = isolated_perp_position_market_index { - context.isolated_position_market_index(isolated_perp_position_market_index); - } - let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, perp_market_map, @@ -703,8 +697,7 @@ impl User { ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; let context = MarginContext::standard(margin_requirement_type) - .strict(strict) - .isolated_position_market_index(isolated_perp_position_market_index); + .strict(strict); let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, From ed76b47bc5219d1de4033f41e9d982aa57dd8657 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 12:54:04 -0400 Subject: [PATCH 022/159] some tweaks --- programs/drift/src/controller/orders.rs | 6 ------ programs/drift/src/instructions/keeper.rs | 2 -- 2 files changed, 8 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 467b27ec16..dc468ac868 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -3079,12 +3079,6 @@ pub fn trigger_order( // If order increases risk and user is below initial margin, cancel it if is_risk_increasing && !user.orders[order_index].reduce_only { - let isolated_position_market_index = if user.get_perp_position(market_index)?.is_isolated() { - Some(market_index) - } else { - None - }; - let meets_initial_margin_requirement = meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 6efb3d7fdc..2591439bfd 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,6 +1,4 @@ use std::cell::RefMut; -use std::collections::BTreeMap; -use std::collections::BTreeSet; use std::convert::TryFrom; use anchor_lang::prelude::*; From c13a605b6a3676a4487a7d9d2e881c40c92c966e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 18:24:05 -0400 Subject: [PATCH 023/159] rm some old margin code --- .../drift/src/controller/pnl/delisting.rs | 14 +++----- programs/drift/src/math/margin.rs | 21 +----------- programs/drift/src/math/margin/tests.rs | 18 ++++------- .../drift/src/state/margin_calculation.rs | 32 ++----------------- 4 files changed, 14 insertions(+), 71 deletions(-) diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index eafbd2148b..67fd12152f 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2336,7 +2336,7 @@ pub mod delisting_test { let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2345,7 +2345,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2416,7 +2415,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, @@ -2425,7 +2424,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2504,7 +2502,7 @@ pub mod delisting_test { assert_eq!(market.amm.cumulative_funding_rate_short, 0); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2513,7 +2511,6 @@ pub mod delisting_test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -2596,7 +2593,7 @@ pub mod delisting_test { assert_eq!(market.amm.cumulative_funding_rate_short, 0); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _, _, _) = + let (perp_margin_requirement, weighted_pnl, _, _) = calculate_perp_position_value_and_pnl( &shorter.perp_positions[0], &market, @@ -2604,8 +2601,7 @@ pub mod delisting_test { &strict_quote_price, MarginRequirementType::Initial, 0, - false, - false, + false ) .unwrap(); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 65dfe7a7a2..4cbbcc5818 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -103,8 +103,7 @@ pub fn calculate_perp_position_value_and_pnl( margin_requirement_type: MarginRequirementType, user_custom_margin_ratio: u32, user_high_leverage_mode: bool, - track_open_order_fraction: bool, -) -> DriftResult<(u128, i128, u128, u128, u128)> { +) -> DriftResult<(u128, i128, u128, u128)> { let valuation_price = if market.status == MarketStatus::Settlement { market.expiry_price } else { @@ -181,22 +180,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, )) } @@ -542,7 +529,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( perp_margin_requirement, weighted_pnl, worst_case_liability_value, - open_order_margin_requirement, base_asset_value, ) = calculate_perp_position_value_and_pnl( market_position, @@ -552,7 +538,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( context.margin_type, user_custom_margin_ratio, user_high_leverage_mode, - calculation.track_open_orders_fraction(), )?; calculation.update_fuel_perp_bonus( @@ -595,10 +580,6 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( MarketIdentifier::perp(market.market_index), )?; - if calculation.track_open_orders_fraction() { - calculation.add_open_orders_margin_requirement(open_order_margin_requirement)?; - } - calculation.add_total_collateral(weighted_pnl)?; } diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 7a256b65db..2ad60a5b9e 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -285,7 +285,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, @@ -293,7 +293,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -363,7 +362,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, @@ -371,7 +370,6 @@ mod test { MarginRequirementType::Initial, 0, false, - false, ) .unwrap(); @@ -4084,7 +4082,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, @@ -4092,14 +4090,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, @@ -4107,7 +4104,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); @@ -4147,7 +4143,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, @@ -4155,14 +4151,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, @@ -4170,7 +4165,6 @@ mod calculate_perp_position_value_and_pnl_prediction_market { MarginRequirementType::Maintenance, 0, false, - false, ) .unwrap(); diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 874b6d4e45..bd1d9d0864 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -17,9 +17,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, }, @@ -65,9 +63,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, @@ -116,21 +112,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", @@ -526,15 +507,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, From 4a9aadc3b9bbe9b2c4bfa227b7b4e1320b7f88cd Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 5 Aug 2025 18:36:43 -0400 Subject: [PATCH 024/159] tweak meets withdraw requirements --- programs/drift/src/instructions/user.rs | 4 ---- .../drift/src/state/margin_calculation.rs | 8 +++++++ programs/drift/src/state/user.rs | 22 +++++++++---------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index c4dbe8eaa2..1d73dcf530 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2232,8 +2232,6 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - user_stats, - now, perp_market_index, )?; @@ -2355,8 +2353,6 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - &mut user_stats, - now, perp_market_index, )?; diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index bd1d9d0864..955b4da458 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -593,4 +593,12 @@ impl MarginCalculation { Ok(()) } + + pub fn get_isolated_position_margin_calculation(&self, market_index: u16) -> DriftResult<&IsolatedPositionMarginCalculation> { + if let Some(isolated_position_margin_calculation) = self.isolated_position_margin_calculation.get(&market_index) { + Ok(isolated_position_margin_calculation) + } else { + Err(ErrorCode::InvalidMarginCalculation) + } + } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 36dd5f7aa3..14e9119e27 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -691,8 +691,6 @@ impl User { spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, margin_requirement_type: MarginRequirementType, - user_stats: &mut UserStats, - now: i64, isolated_perp_position_market_index: u16, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; @@ -707,20 +705,20 @@ impl User { context, )?; - if calculation.margin_requirement > 0 || calculation.get_num_of_liabilities()? > 0 { - validate!( - calculation.all_liability_oracles_valid, - ErrorCode::InvalidOracle, - "User attempting to withdraw with outstanding liabilities when an oracle is invalid" - )?; - } + let isolated_position_margin_calculation = calculation.get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; validate!( - calculation.meets_margin_requirement(), + calculation.all_liability_oracles_valid, + ErrorCode::InvalidOracle, + "User attempting to withdraw with outstanding liabilities when an oracle is invalid" + )?; + + validate!( + isolated_position_margin_calculation.meets_margin_requirement(), ErrorCode::InsufficientCollateral, "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - calculation.total_collateral, - calculation.margin_requirement + isolated_position_margin_calculation.total_collateral, + isolated_position_margin_calculation.margin_requirement )?; Ok(true) From 0d564880a80f6b7547c21e7ee820d1b1f2860cc7 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 6 Aug 2025 10:30:01 -0400 Subject: [PATCH 025/159] rm liquidation mode changing context --- programs/drift/src/controller/liquidation.rs | 34 +++++++------------- programs/drift/src/state/liquidation_mode.rs | 10 ------ 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 5a383254e7..95e77b0599 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -147,7 +147,8 @@ pub fn liquidate_perp( perp_market_map, spot_market_map, oracle_map, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; @@ -226,14 +227,14 @@ pub fn liquidate_perp( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -548,7 +549,6 @@ pub fn liquidate_perp( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); @@ -777,13 +777,13 @@ pub fn liquidate_perp_with_fill( now, )?; - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { @@ -851,14 +851,14 @@ pub fn liquidate_perp_with_fill( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?.track_market_margin_requirement(MarketIdentifier::perp(market_index))?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -1103,7 +1103,6 @@ pub fn liquidate_perp_with_fill( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; @@ -1673,7 +1672,6 @@ pub fn liquidate_spot( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - MarginContext::liquidation(liquidation_margin_buffer_ratio) )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; @@ -2246,7 +2244,6 @@ pub fn liquidate_spot_with_swap_end( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - MarginContext::liquidation(liquidation_margin_buffer_ratio) )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; @@ -2707,8 +2704,6 @@ pub fn liquidate_borrow_for_perp_pnl( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - MarginContext::liquidation(liquidation_margin_buffer_ratio) - )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; @@ -2945,13 +2940,12 @@ pub fn liquidate_perp_pnl_for_deposit( ) }; - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; if !liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.meets_margin_requirement() { @@ -2989,14 +2983,13 @@ pub fn liquidate_perp_pnl_for_deposit( // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() { - let margin_context = liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?; let intermediate_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; let initial_margin_shortage = margin_calculation.margin_shortage()?; @@ -3194,7 +3187,6 @@ pub fn liquidate_perp_pnl_for_deposit( oracle_map, liquidation_margin_buffer_ratio, margin_shortage, - liquidation_mode.get_margin_context(liquidation_margin_buffer_ratio)?, )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; liquidation_mode.increment_free_margin(user, margin_freed_from_liability); @@ -3309,7 +3301,6 @@ pub fn resolve_perp_bankruptcy( "user must have negative pnl" )?; - let margin_context = liquidation_mode.get_margin_context(0)?; let MarginCalculation { margin_requirement, total_collateral, @@ -3319,7 +3310,7 @@ pub fn resolve_perp_bankruptcy( perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::standard(MarginRequirementType::Maintenance), )?; // spot market's insurance fund draw attempt here (before social loss) @@ -3632,7 +3623,6 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, - margin_context: MarginContext, ) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( @@ -3640,7 +3630,7 @@ pub fn calculate_margin_freed( perp_market_map, spot_market_map, oracle_map, - margin_context, + MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; let new_margin_shortage = margin_calculation_after.margin_shortage()?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index edf8b99c7c..8a950f3404 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -5,8 +5,6 @@ use crate::{controller::{spot_balance::update_spot_balances, spot_position::upda use super::{perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; pub trait LiquidatePerpMode { - fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult; - fn user_is_being_liquidated(&self, user: &User) -> DriftResult; fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; @@ -62,10 +60,6 @@ impl CrossMarginLiquidatePerpMode { } impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { - fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) - } - fn user_is_being_liquidated(&self, user: &User) -> DriftResult { Ok(user.is_being_liquidated()) } @@ -186,10 +180,6 @@ impl IsolatedLiquidatePerpMode { } impl LiquidatePerpMode for IsolatedLiquidatePerpMode { - fn get_margin_context(&self, liquidation_margin_buffer_ratio: u32) -> DriftResult { - Ok(MarginContext::liquidation(liquidation_margin_buffer_ratio)) - } - fn user_is_being_liquidated(&self, user: &User) -> DriftResult { user.is_isolated_position_being_liquidated(self.market_index) } From 584337bf85c0c2af827164779f0935e856af9c10 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 7 Aug 2025 12:52:44 -0400 Subject: [PATCH 026/159] handle liquidation id and bit flags --- programs/drift/src/controller/liquidation.rs | 7 ++++ programs/drift/src/state/events.rs | 6 +++ programs/drift/src/state/liquidation_mode.rs | 12 +++++- programs/drift/src/state/user.rs | 29 +++++++++++-- programs/drift/src/state/user/tests.rs | 43 ++++++++++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 95e77b0599..9364dbfeaf 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -263,6 +263,7 @@ pub fn liquidate_perp( lp_shares: 0, ..LiquidatePerpRecord::default() }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -699,6 +700,7 @@ pub fn liquidate_perp( liquidator_fee: liquidator_fee.abs().cast()?, if_fee: if_fee.abs().cast()?, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -887,6 +889,7 @@ pub fn liquidate_perp_with_fill( lp_shares: 0, ..LiquidatePerpRecord::default() }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -1143,6 +1146,7 @@ pub fn liquidate_perp_with_fill( liquidator_fee: 0, if_fee: if_fee.abs().cast()?, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -3025,6 +3029,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer: 0, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -3229,6 +3234,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); @@ -3456,6 +3462,7 @@ pub fn resolve_perp_bankruptcy( clawback_user_payment: None, cumulative_funding_rate_delta, }, + bit_flags: liquidation_mode.get_event_bit_flags(), ..LiquidationRecord::default() }); diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 8f93fae37e..9d4fd22fca 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -425,6 +425,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)] @@ -506,6 +507,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 index 8a950f3404..f0d93513c3 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -2,7 +2,7 @@ use solana_program::msg; use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers}, state::margin_calculation::{MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; -use super::{perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; +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; @@ -30,6 +30,8 @@ pub trait LiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; + fn get_event_bit_flags(&self) -> 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; @@ -109,6 +111,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.exit_bankruptcy()) } + fn get_event_bit_flags(&self) -> u8 { + 0 + } + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { if user.get_spot_position(asset_market_index).is_err() { msg!( @@ -223,6 +229,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.exit_isolated_position_bankruptcy(self.market_index) } + fn get_event_bit_flags(&self) -> u8 { + LiquidationBitFlag::IsolatedPosition as u8 + } + fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { validate!( asset_market_index == QUOTE_SPOT_MARKET_INDEX, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 14e9119e27..326e48bcbc 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -377,7 +377,15 @@ impl User { 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.any_isolated_position_being_liquidated() { + self.next_liquidation_id.safe_sub(1)? + } else { + get_then_update_id!(self, next_liquidation_id) + }; + + Ok(liquidation_id) } pub fn exit_liquidation(&mut self) { @@ -397,17 +405,26 @@ impl User { self.liquidation_margin_freed = 0; } + fn any_isolated_position_being_liquidated(&self) -> bool { + self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) + } + pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { - // todo figure out liquidation id if self.is_isolated_position_being_liquidated(perp_market_index)? { return self.next_liquidation_id.safe_sub(1); } + let liquidation_id = if self.is_being_liquidated() || self.any_isolated_position_being_liquidated() { + self.next_liquidation_id.safe_sub(1)? + } else { + 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(get_then_update_id!(self, next_liquidation_id)) + Ok(liquidation_id) } pub fn exit_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { @@ -418,7 +435,7 @@ impl User { pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0) + Ok(perp_position.is_isolated_position_being_liquidated()) } pub fn enter_isolated_position_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { @@ -1240,6 +1257,10 @@ impl PerpPosition { pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { get_token_amount(self.isolated_position_scaled_balance as u128, spot_market, &SpotBalanceType::Deposit) } + + pub fn is_isolated_position_being_liquidated(&self) -> bool { + self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 + } } impl SpotBalance for PerpPosition { diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 57a8654086..94312c9e63 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2309,3 +2309,46 @@ mod update_referrer_status { assert_eq!(user_stats.referrer_status, 1); } } + +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_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 1); + + let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 1); + + user.exit_isolated_position_liquidation(1).unwrap(); + + user.exit_liquidation(); + + let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 2); + + let liquidation_id = user.enter_isolated_position_liquidation(2).unwrap(); + assert_eq!(liquidation_id, 2); + + let liquidation_id = user.enter_liquidation(1).unwrap(); + assert_eq!(liquidation_id, 2); + } +} \ No newline at end of file From 15c05eeea188c93eb5a48f584cab8951e8aedb38 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 7 Aug 2025 17:42:14 -0400 Subject: [PATCH 027/159] more liquidation changes --- programs/drift/src/controller/liquidation.rs | 87 ++++++++++--------- programs/drift/src/math/liquidation.rs | 4 +- programs/drift/src/state/liquidation_mode.rs | 43 ++++++--- .../drift/src/state/margin_calculation.rs | 55 +++++++----- 4 files changed, 114 insertions(+), 75 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 9364dbfeaf..63c488d9a7 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -152,10 +152,10 @@ pub fn liquidate_perp( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && margin_calculation.meets_margin_requirement() { + 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()? { + } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -245,15 +245,16 @@ pub fn liquidate_perp( .cast::()?; 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(&intermediate_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, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -263,7 +264,7 @@ pub fn liquidate_perp( lp_shares: 0, ..LiquidatePerpRecord::default() }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -677,14 +678,15 @@ 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, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -700,7 +702,7 @@ pub fn liquidate_perp( liquidator_fee: liquidator_fee.abs().cast()?, if_fee: if_fee.abs().cast()?, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -788,10 +790,11 @@ pub fn liquidate_perp_with_fill( .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, )?; - if !liquidation_mode.user_is_being_liquidated(&user)? && 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 liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { + } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -871,15 +874,16 @@ pub fn liquidate_perp_with_fill( .cast::()?; 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(&intermediate_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, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -889,11 +893,11 @@ pub fn liquidate_perp_with_fill( lp_shares: 0, ..LiquidatePerpRecord::default() }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); - user.exit_liquidation(); + liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -1123,14 +1127,15 @@ 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, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -1146,7 +1151,7 @@ pub fn liquidate_perp_with_fill( liquidator_fee: 0, if_fee: if_fee.abs().cast()?, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -1390,7 +1395,7 @@ pub fn liquidate_spot( if !user.is_being_liquidated() && margin_calculation.meets_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_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { user.exit_liquidation(); return Ok(()); } @@ -1437,7 +1442,7 @@ pub fn liquidate_spot( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1921,7 +1926,7 @@ pub fn liquidate_spot_with_swap_begin( if !user.is_being_liquidated() && margin_calculation.meets_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_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); } @@ -1991,7 +1996,7 @@ pub fn liquidate_spot_with_swap_begin( }); // must throw error to stop swap - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { return Err(ErrorCode::InvalidLiquidation); } @@ -2253,7 +2258,7 @@ pub fn liquidate_spot_with_swap_end( 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()? { + if margin_calulcation_after.cross_margin_can_exit_liquidation()? { user.exit_liquidation(); } else if is_user_bankrupt(user) { user.enter_bankruptcy(); @@ -2493,7 +2498,7 @@ pub fn liquidate_borrow_for_perp_pnl( if !user.is_being_liquidated() && margin_calculation.meets_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_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { user.exit_liquidation(); return Ok(()); } @@ -2536,7 +2541,7 @@ pub fn liquidate_borrow_for_perp_pnl( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.can_exit_liquidation()? { + if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { let market = perp_market_map.get_ref(&perp_market_index)?; let market_oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; @@ -2952,10 +2957,11 @@ pub fn liquidate_perp_pnl_for_deposit( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !liquidation_mode.user_is_being_liquidated(&user)? && 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 liquidation_mode.user_is_being_liquidated(&user)? && margin_calculation.can_exit_liquidation()? { + } else if user_is_being_liquidated && liquidation_mode.can_exit_liquidation(&margin_calculation)? { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -3004,20 +3010,21 @@ pub fn liquidate_perp_pnl_for_deposit( .cast::()?; 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(&intermediate_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, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), canceled_order_ids, margin_freed, @@ -3029,7 +3036,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer: 0, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -3216,14 +3223,15 @@ 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, + margin_requirement, + total_collateral, bankrupt: user.is_bankrupt(), margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { @@ -3234,7 +3242,7 @@ pub fn liquidate_perp_pnl_for_deposit( asset_price, asset_transfer, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); @@ -3307,11 +3315,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, @@ -3445,6 +3449,7 @@ pub fn resolve_perp_bankruptcy( 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, @@ -3462,7 +3467,7 @@ pub fn resolve_perp_bankruptcy( clawback_user_payment: None, cumulative_funding_rate_delta, }, - bit_flags: liquidation_mode.get_event_bit_flags(), + bit_flags, ..LiquidationRecord::default() }); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index cf425bade7..d58da27314 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -213,7 +213,7 @@ pub fn is_user_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.cross_margin_can_exit_liquidation()?; Ok(is_being_liquidated) } @@ -281,7 +281,7 @@ pub fn is_isolated_position_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.cross_margin_can_exit_liquidation()?; Ok(is_being_liquidated) } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index f0d93513c3..1fd5a4d816 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -1,12 +1,16 @@ use solana_program::msg; -use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers}, state::margin_calculation::{MarginContext, MarketIdentifier}, validate, 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_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap}, state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; use super::{events::LiquidationBitFlag, perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; pub trait LiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult; + fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult; + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult; + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; fn get_cancel_orders_params(&self) -> (Option, Option, bool); @@ -20,7 +24,7 @@ pub trait LiquidatePerpMode { liquidation_duration: u128, ) -> DriftResult; - fn increment_free_margin(&self, user: &mut User, amount: u64); + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()>; fn is_user_bankrupt(&self, user: &User) -> DriftResult; @@ -30,7 +34,7 @@ pub trait LiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; - fn get_event_bit_flags(&self) -> u8; + fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)>; fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()>; @@ -66,6 +70,14 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.is_being_liquidated()) } + fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + Ok(margin_calculation.cross_margin_meets_margin_requirement()) + } + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { + Ok(margin_calculation.cross_margin_can_exit_liquidation()?) + } + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { Ok(user.exit_liquidation()) } @@ -91,8 +103,8 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { ) } - fn increment_free_margin(&self, user: &mut User, amount: u64) { - user.increment_margin_freed(amount); + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()> { + user.increment_margin_freed(amount) } fn is_user_bankrupt(&self, user: &User) -> DriftResult { @@ -111,8 +123,8 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.exit_bankruptcy()) } - fn get_event_bit_flags(&self) -> u8 { - 0 + 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<()> { @@ -190,6 +202,14 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.is_isolated_position_being_liquidated(self.market_index) } + fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.isolated_position_meets_margin_requirement(self.market_index) + } + + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.isolated_position_can_exit_liquidation(self.market_index) + } + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { user.exit_isolated_position_liquidation(self.market_index) } @@ -209,8 +229,8 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { Ok(LIQUIDATION_PCT_PRECISION) } - fn increment_free_margin(&self, user: &mut User, amount: u64) { - return; + fn increment_free_margin(&self, user: &mut User, amount: u64) -> DriftResult<()> { + Ok(()) } fn is_user_bankrupt(&self, user: &User) -> DriftResult { @@ -229,8 +249,9 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.exit_isolated_position_bankruptcy(self.market_index) } - fn get_event_bit_flags(&self) -> u8 { - LiquidationBitFlag::IsolatedPosition as u8 + fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { + let isolated_position_margin_calculation = margin_calculation.isolated_position_margin_calculation.get(&self.market_index).safe_unwrap()?; + Ok((isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral, LiquidationBitFlag::IsolatedPosition as u8)) } fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 955b4da458..d79eb0b030 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -5,6 +5,7 @@ use crate::math::casting::Cast; use crate::math::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; use crate::math::margin::MarginRequirementType; use crate::math::safe_math::SafeMath; +use crate::math::safe_unwrap::SafeUnwrap; use crate::math::spot_balance::get_strict_token_value; use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::PerpMarket; @@ -182,7 +183,6 @@ 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, @@ -231,7 +231,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, @@ -315,13 +314,6 @@ 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)?; - Ok(()) - } - pub fn add_spot_liability(&mut self) -> DriftResult { self.num_spot_liabilities = self.num_spot_liabilities.safe_add(1)?; Ok(()) @@ -400,13 +392,13 @@ impl MarginCalculation { } pub fn meets_margin_requirement(&self) -> bool { - let cross_margin_meets_margin_requirement = self.total_collateral >= self.margin_requirement as i128; + let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement(); if !cross_margin_meets_margin_requirement { return false; } - for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement() { return false; } @@ -416,13 +408,13 @@ impl MarginCalculation { } pub fn meets_margin_requirement_with_buffer(&self) -> bool { - let cross_margin_meets_margin_requirement = self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128; + let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement_with_buffer(); if !cross_margin_meets_margin_requirement { return false; } - for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { return false; } @@ -438,21 +430,42 @@ impl MarginCalculation { } } - 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 cross_margin_meets_margin_requirement(&self) -> bool { + self.total_collateral >= self.margin_requirement as i128 + } + + #[inline(always)] + pub fn cross_margin_meets_margin_requirement_with_buffer(&self) -> bool { + self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + } + + #[inline(always)] + pub fn isolated_position_meets_margin_requirement(&self, market_index: u16) -> DriftResult { + Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement()) + } + + #[inline(always)] + pub fn isolated_position_meets_margin_requirement_with_buffer(&self, market_index: u16) -> DriftResult { + Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) } - pub fn can_exit_liquidation(&self) -> DriftResult { + pub fn cross_margin_can_exit_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.cross_margin_meets_margin_requirement_with_buffer()) + } + + pub fn isolated_position_can_exit_liquidation(&self, market_index: u16) -> DriftResult { + if !self.is_liquidation_mode() { + msg!("liquidation mode not enabled"); + return Err(ErrorCode::InvalidMarginCalculation); + } + + Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) } pub fn margin_shortage(&self) -> DriftResult { From adc28151d618cd8c6cd632de8d4a8d05ee355f15 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 08:59:11 -0400 Subject: [PATCH 028/159] clean --- programs/drift/src/controller/pnl.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 65d6364be3..d450a2c11e 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -84,8 +84,7 @@ pub fn settle_pnl( // may already be cached let meets_margin_requirement = match meets_margin_requirement { Some(meets_margin_requirement) => meets_margin_requirement, - // TODO check margin for isolate position - _ => meets_settle_pnl_maintenance_margin_requirement( + None => meets_settle_pnl_maintenance_margin_requirement( user, perp_market_map, spot_market_map, From 0de7802976fa2d8ba9930092791817e86168720a Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 09:08:09 -0400 Subject: [PATCH 029/159] fix force cancel orders --- programs/drift/src/controller/orders.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index dc468ac868..89dda68411 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -3191,6 +3191,8 @@ pub fn force_cancel_orders( ErrorCode::SufficientCollateral )?; + let cross_margin_meets_initial_margin_requirement = margin_calc.cross_margin_meets_margin_requirement(); + let mut total_fee = 0_u64; for order_index in 0..user.orders.len() { @@ -3217,6 +3219,10 @@ pub fn force_cancel_orders( continue; } + if cross_margin_meets_initial_margin_requirement { + continue; + } + state.spot_fee_structure.flat_filler_fee } MarketType::Perp => { @@ -3231,9 +3237,15 @@ pub fn force_cancel_orders( continue; } - // TODO: handle force deleting these orders - if user.get_perp_position(market_index)?.is_isolated() { - continue; + if !user.get_perp_position(market_index)?.is_isolated() { + if cross_margin_meets_initial_margin_requirement { + continue; + } + } else { + let isolated_position_meets_margin_requirement = margin_calc.isolated_position_meets_margin_requirement(market_index)?; + if isolated_position_meets_margin_requirement { + continue; + } } state.perp_fee_structure.flat_filler_fee From 830c7c90841245dc901b80b09cb6005be668b251 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 15:36:46 -0400 Subject: [PATCH 030/159] update validate liquidation --- programs/drift/src/controller/orders.rs | 6 --- programs/drift/src/instructions/user.rs | 1 - programs/drift/src/math/liquidation.rs | 53 +++++++++++-------------- programs/drift/src/state/user.rs | 2 +- 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 89dda68411..0607380dd2 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -115,7 +115,6 @@ pub fn place_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - Some(params.market_index), )?; } @@ -1041,7 +1040,6 @@ pub fn fill_perp_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - Some(market_index), ) { Ok(_) => {} Err(_) => { @@ -2953,7 +2951,6 @@ pub fn trigger_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - Some(market_index), )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -3383,7 +3380,6 @@ pub fn place_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - None, )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -3727,7 +3723,6 @@ pub fn fill_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - None, ) { Ok(_) => {} Err(_) => { @@ -5208,7 +5203,6 @@ pub fn trigger_spot_order( spot_market_map, oracle_map, state.liquidation_margin_buffer_ratio, - None, )?; validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 1d73dcf530..b8eee90d7e 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3793,7 +3793,6 @@ pub fn handle_begin_swap<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, ctx.accounts.state.liquidation_margin_buffer_ratio, - None, )?; let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index d58da27314..e60f6a60a1 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -224,41 +224,34 @@ pub fn validate_user_not_being_liquidated( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, - perp_market_index: Option, ) -> DriftResult { - if !user.is_being_liquidated() { + if !user.is_being_liquidated() && !user.any_isolated_position_being_liquidated() { return Ok(()); } - let is_isolated_perp_market = if let Some(perp_market_index) = perp_market_index { - user.force_get_perp_position_mut(perp_market_index)?.is_isolated() - } else { - false - }; - - let is_still_being_liquidated = if is_isolated_perp_market { - is_isolated_position_being_liquidated( - user, - market_map, - spot_market_map, - oracle_map, - perp_market_index.unwrap(), - liquidation_margin_buffer_ratio, - )? - } else { - is_user_being_liquidated( - user, - market_map, - spot_market_map, - oracle_map, - liquidation_margin_buffer_ratio, - )? - }; + 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), + )?; - if is_still_being_liquidated { - return Err(ErrorCode::UserIsBeingLiquidated); + if user.is_being_liquidated() { + if margin_calculation.cross_margin_can_exit_liquidation()? { + user.exit_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_isolated_position_being_liquidated()).map(|position| position.market_index).collect::>(); + for perp_market_index in isolated_positions_being_liquidated { + if margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } else { + return Err(ErrorCode::UserIsBeingLiquidated); + } + } } Ok(()) @@ -281,7 +274,7 @@ pub fn is_isolated_position_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.cross_margin_can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; Ok(is_being_liquidated) } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 326e48bcbc..2c65ad60b3 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -405,7 +405,7 @@ impl User { self.liquidation_margin_freed = 0; } - fn any_isolated_position_being_liquidated(&self) -> bool { + pub fn any_isolated_position_being_liquidated(&self) -> bool { self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) } From 5d097399cc9694254f6bc2679a2f216834ce830b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 19:06:15 -0400 Subject: [PATCH 031/159] moar --- programs/drift/src/controller/liquidation.rs | 54 +++++++++------- .../drift/src/controller/liquidation/tests.rs | 4 +- programs/drift/src/math/margin.rs | 4 +- programs/drift/src/math/orders.rs | 15 ++--- programs/drift/src/state/liquidation_mode.rs | 10 +++ .../drift/src/state/margin_calculation.rs | 64 ++++++++++++++----- 6 files changed, 101 insertions(+), 50 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 63c488d9a7..0d7164b986 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -237,13 +237,13 @@ 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::()?; - liquidation_mode.increment_free_margin(user, margin_freed); + liquidation_mode.increment_free_margin(user, margin_freed)?; if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; @@ -325,7 +325,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)?; @@ -551,6 +551,7 @@ 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)?; liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position); @@ -866,8 +867,8 @@ 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) @@ -938,7 +939,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)?; @@ -1110,6 +1111,7 @@ 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)?; @@ -1434,8 +1436,8 @@ 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) @@ -1475,7 +1477,7 @@ pub fn liquidate_spot( 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)?; @@ -1681,7 +1683,7 @@ 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)?; @@ -1964,8 +1966,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) @@ -2005,7 +2007,7 @@ pub fn liquidate_spot_with_swap_begin( 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)?; @@ -2212,7 +2214,7 @@ pub fn liquidate_spot_with_swap_end( let liquidation_id = user.enter_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::()? @@ -2253,6 +2255,7 @@ 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)?; @@ -2533,8 +2536,8 @@ 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) @@ -2576,7 +2579,7 @@ pub fn liquidate_borrow_for_perp_pnl( 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,6 +2716,7 @@ 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)?; @@ -3002,8 +3006,8 @@ 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) @@ -3069,7 +3073,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)?; @@ -3199,6 +3203,7 @@ 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)?; liquidation_mode.increment_free_margin(user, margin_freed_from_liability); @@ -3635,6 +3640,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( @@ -3645,7 +3651,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) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index e8cf21acde..af47cf0565 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -6873,7 +6873,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 +6914,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); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 4cbbcc5818..6e12af8ca4 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -707,7 +707,7 @@ pub fn meets_place_order_margin_requirement( )?; if !calculation.meets_margin_requirement() { - calculation.print_margin_calculations(); + msg!("margin calculation: {:?}", calculation); return Err(ErrorCode::InsufficientCollateral); } @@ -803,7 +803,7 @@ pub fn calculate_max_withdrawable_amount( return token_amount.cast(); } - let free_collateral = calculation.get_free_collateral()?; + let free_collateral = calculation.get_cross_margin_free_collateral()?; let (numerator_scale, denominator_scale) = if spot_market.decimals > 6 { (10_u128.pow(spot_market.decimals - 6), 1) diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 879bc4e9fe..544480be2e 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -845,14 +845,9 @@ pub fn calculate_max_perp_order_size( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, ) -> DriftResult { - let is_isolated_position = user.perp_positions[position_index].is_isolated(); 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, @@ -863,8 +858,12 @@ pub fn calculate_max_perp_order_size( let user_custom_margin_ratio = user.max_margin_ratio; let user_high_leverage_mode = user.is_high_leverage_mode(); - // todo check if this is correct - 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_position_free_collateral(market_index)?.cast::()? + } else { + margin_calculation.get_cross_margin_free_collateral()?.cast::()? + }; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 1fd5a4d816..28cdb854ec 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -49,6 +49,8 @@ pub trait LiquidatePerpMode { 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) -> Box { @@ -185,6 +187,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(()) } + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.cross_margin_margin_shortage() + } } pub struct IsolatedLiquidatePerpMode { @@ -302,4 +308,8 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { Ok(()) } + + fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { + margin_calculation.isolated_position_margin_shortage(self.market_index) + } } \ No newline at end of file diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index d79eb0b030..879e50997b 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -210,6 +210,10 @@ impl IsolatedPositionMarginCalculation { 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())?.unsigned_abs()) + } } impl MarginCalculation { @@ -280,7 +284,7 @@ impl MarginCalculation { } pub fn add_isolated_position_margin_calculation(&mut self, market_index: u16, deposit_value: i128, pnl: i128, liability_value: u128, margin_requirement: u128) -> DriftResult { - let total_collateral = deposit_value.cast::()?.safe_add(pnl)?; + 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 @@ -423,13 +427,6 @@ impl MarginCalculation { true } - pub fn print_margin_calculations(&self) { - msg!("cross_margin margin_requirement={}, total_collateral={}", self.margin_requirement, self.total_collateral); - for (market_index, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { - msg!("isolated_position for market {}: margin_requirement={}, total_collateral={}", market_index, isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral); - } - } - #[inline(always)] pub fn cross_margin_meets_margin_requirement(&self) -> bool { self.total_collateral >= self.margin_requirement as i128 @@ -468,7 +465,7 @@ impl MarginCalculation { Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) } - pub fn margin_shortage(&self) -> DriftResult { + pub fn cross_margin_margin_shortage(&self) -> DriftResult { if self.context.margin_buffer == 0 { msg!("margin buffer mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); @@ -481,29 +478,64 @@ impl MarginCalculation { .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_position_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_position_margin_calculation.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_position_margin_calculation.get(&market_index) { + Some(isolated_position_margin_calculation) => { + isolated_position_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) } - // todo check every where this is used - pub fn get_free_collateral(&self) -> DriftResult { + pub fn get_cross_margin_free_collateral(&self) -> DriftResult { self.total_collateral .safe_sub(self.margin_requirement.cast::()?)? .max(0) .cast() } + pub fn get_isolated_position_free_collateral(&self, market_index: u16) -> DriftResult { + let isolated_position_margin_calculation = self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?; + isolated_position_margin_calculation.total_collateral + .safe_sub(isolated_position_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, From 7392d3e81d735793f3966790d9fa5813038d2882 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 8 Aug 2025 19:26:07 -0400 Subject: [PATCH 032/159] rename is_being_liquidated --- programs/drift/src/controller/liquidation.rs | 112 +++++++++--------- .../drift/src/controller/liquidation/tests.rs | 12 +- programs/drift/src/controller/orders.rs | 40 +++++-- programs/drift/src/controller/pnl.rs | 4 +- .../drift/src/controller/pnl/delisting.rs | 16 +-- programs/drift/src/instructions/admin.rs | 2 +- programs/drift/src/instructions/user.rs | 58 ++++----- programs/drift/src/math/liquidation.rs | 6 +- programs/drift/src/state/liquidation_mode.rs | 8 +- .../drift/src/state/margin_calculation.rs | 4 + programs/drift/src/state/user.rs | 24 ++-- programs/drift/src/state/user/tests.rs | 32 ++--- programs/drift/src/validation/user.rs | 4 +- 13 files changed, 172 insertions(+), 150 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 0d7164b986..05cc104eca 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -105,7 +105,7 @@ pub fn liquidate_perp( )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -178,7 +178,7 @@ pub fn liquidate_perp( e })?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -255,7 +255,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -688,7 +688,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -752,7 +752,7 @@ pub fn liquidate_perp_with_fill( )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -808,7 +808,7 @@ pub fn liquidate_perp_with_fill( e })?; - let liquidation_id = user.enter_liquidation(slot)?; + let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -885,7 +885,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1138,7 +1138,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1183,13 +1183,13 @@ 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", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -1394,15 +1394,15 @@ 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_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_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( @@ -1453,7 +1453,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 { @@ -1468,7 +1468,7 @@ pub fn liquidate_spot( ..LiquidationRecord::default() }); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); return Ok(()); } @@ -1689,9 +1689,9 @@ pub fn liquidate_spot( user.increment_margin_freed(margin_freed_from_liability)?; if liability_transfer >= liability_transfer_to_cover_margin_shortage { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); } let liq_margin_context = MarginContext::standard(MarginRequirementType::Initial) @@ -1726,7 +1726,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,13 +1765,13 @@ 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", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -1925,15 +1925,15 @@ 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_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_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, @@ -1982,7 +1982,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 { @@ -2211,7 +2211,7 @@ 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.cross_margin_margin_shortage()?; @@ -2262,9 +2262,9 @@ pub fn liquidate_spot_with_swap_end( user.increment_margin_freed(margin_freed_from_liability)?; if margin_calulcation_after.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); } emit!(LiquidationRecord { @@ -2275,7 +2275,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, @@ -2315,13 +2315,13 @@ 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", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2498,15 +2498,15 @@ 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_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_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( @@ -2556,7 +2556,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 { @@ -2570,7 +2570,7 @@ pub fn liquidate_borrow_for_perp_pnl( ..LiquidationRecord::default() }); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); return Ok(()); } @@ -2722,9 +2722,9 @@ pub fn liquidate_borrow_for_perp_pnl( user.increment_margin_freed(margin_freed_from_liability)?; if liability_transfer >= liability_transfer_to_cover_margin_shortage { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else if is_user_bankrupt(user) { - user.enter_bankruptcy(); + user.enter_cross_margin_bankruptcy(); } let liquidator_meets_initial_margin_requirement = @@ -2749,7 +2749,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, @@ -2797,7 +2797,7 @@ pub fn liquidate_perp_pnl_for_deposit( )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2970,7 +2970,7 @@ pub fn liquidate_perp_pnl_for_deposit( 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 (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); @@ -3029,7 +3029,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), canceled_order_ids, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { @@ -3237,7 +3237,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_bankrupt(), + bankrupt: user.is_cross_margin_bankrupt(), margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { perp_market_index, @@ -3279,13 +3279,13 @@ pub fn resolve_perp_bankruptcy( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_cross_margin_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -3491,24 +3491,24 @@ 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_user_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(), + !liquidator.is_cross_margin_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; validate!( - !liquidator.is_bankrupt(), + !liquidator.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -3607,7 +3607,7 @@ pub fn resolve_spot_bankruptcy( // exit bankruptcy if !is_user_bankrupt(user) { - user.exit_bankruptcy(); + user.exit_cross_margin_bankruptcy(); } let liquidation_id = user.next_liquidation_id.safe_sub(1)?; @@ -3673,13 +3673,13 @@ pub fn set_user_status_to_being_liquidated( state: &State, ) -> DriftResult { validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt", )?; validate!( - !user.is_being_liquidated(), + !user.is_cross_margin_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "user is already being liquidated", )?; @@ -3694,8 +3694,8 @@ pub fn set_user_status_to_being_liquidated( )?; // todo handle this - if !user.is_being_liquidated() && !margin_calculation.meets_margin_requirement() { - user.enter_liquidation(slot)?; + if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_margin_requirement() { + user.enter_cross_margin_liquidation(slot)?; } let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index af47cf0565..cc815592f3 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -2197,7 +2197,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 +2351,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)) @@ -4256,7 +4256,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 +4326,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 +4353,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] @@ -8560,7 +8560,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(); diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 0607380dd2..ca09018b8c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -118,7 +118,7 @@ pub fn place_perp_order( )?; } - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; if params.is_update_high_leverage_mode() { if let Some(config) = high_leverage_mode_config { @@ -1028,7 +1028,7 @@ pub fn fill_perp_order( "Order must be triggered first" )?; - if user.is_bankrupt() { + if user.is_cross_margin_bankrupt() { msg!("user is bankrupt"); return Ok((0, 0)); } @@ -1479,7 +1479,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; } @@ -1945,10 +1945,17 @@ fn fulfill_perp_order( )?; if !taker_margin_calculation.meets_margin_requirement() { + let (margin_requirement, total_collateral) = if taker_margin_calculation.has_isolated_position_margin_calculation(market_index) { + let isolated_position_margin_calculation = taker_margin_calculation.get_isolated_position_margin_calculation(market_index)?; + (isolated_position_margin_calculation.margin_requirement, isolated_position_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); } @@ -2005,11 +2012,18 @@ fn fulfill_perp_order( } if !maker_margin_calculation.meets_margin_requirement() { + let (margin_requirement, total_collateral) = if maker_margin_calculation.has_isolated_position_margin_calculation(market_index) { + let isolated_position_margin_calculation = maker_margin_calculation.get_isolated_position_margin_calculation(market_index)?; + (isolated_position_margin_calculation.margin_requirement, isolated_position_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); } @@ -2953,7 +2967,7 @@ pub fn trigger_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let mut perp_market = perp_market_map.get_ref_mut(&market_index)?; let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( @@ -3171,7 +3185,7 @@ pub fn force_cancel_orders( ErrorCode::UserIsBeingLiquidated )?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -3382,7 +3396,7 @@ pub fn place_spot_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; if options.try_expire_orders { expire_orders( @@ -3712,7 +3726,7 @@ pub fn fill_spot_order( "Order must be triggered first" )?; - if user.is_bankrupt() { + if user.is_cross_margin_bankrupt() { msg!("User is bankrupt"); return Ok(0); } @@ -4020,7 +4034,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; } @@ -5205,7 +5219,7 @@ pub fn trigger_spot_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let spot_market = spot_market_map.get_ref(&market_index)?; let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index d450a2c11e..ea6df2ed68 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -59,7 +59,7 @@ pub fn settle_pnl( meets_margin_requirement: Option, mode: SettlePnlMode, ) -> DriftResult { - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let now = clock.unix_timestamp; { let spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; @@ -346,7 +346,7 @@ pub fn settle_expired_position( clock: &Clock, state: &State, ) -> DriftResult { - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; // cannot settle pnl this way on a user who is in liquidation territory if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index 67fd12152f..f781b93d1e 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2382,8 +2382,8 @@ pub mod delisting_test { let mut shorter_user_stats = UserStats::default(); let mut liq_user_stats = UserStats::default(); - assert_eq!(shorter.is_being_liquidated(), false); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), false); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); let state = State { liquidation_margin_buffer_ratio: 10, ..Default::default() @@ -2407,8 +2407,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 market = market_map.get_ref_mut(&0).unwrap(); @@ -2489,8 +2489,8 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), false); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), false); { let mut market = market_map.get_ref_mut(&0).unwrap(); @@ -2580,8 +2580,8 @@ pub mod delisting_test { ) .unwrap(); - assert_eq!(shorter.is_being_liquidated(), true); - assert_eq!(shorter.is_bankrupt(), true); + assert_eq!(shorter.is_cross_margin_being_liquidated(), true); + assert_eq!(shorter.is_cross_margin_bankrupt(), true); { let market = market_map.get_ref_mut(&0).unwrap(); diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 669a6d95b2..00e0e645fd 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4612,7 +4612,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index b8eee90d7e..2fa326edc1 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -532,7 +532,7 @@ pub fn handle_deposit<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; @@ -614,7 +614,7 @@ 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( user, @@ -625,7 +625,7 @@ pub fn handle_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } } @@ -712,7 +712,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let spot_market_is_reduce_only = { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; @@ -791,8 +791,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); @@ -882,13 +882,13 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_bankrupt(), + !to_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_bankrupt(), + !from_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -970,8 +970,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); @@ -1104,12 +1104,12 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( let clock = Clock::get()?; validate!( - !to_user.is_bankrupt(), + !to_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_bankrupt(), + !from_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1455,12 +1455,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)?; @@ -1577,13 +1577,13 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_bankrupt(), + !to_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_bankrupt(), + !from_user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1938,7 +1938,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; let perp_market = perp_market_map.get_ref(&perp_market_index)?; @@ -2090,7 +2090,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt" )?; @@ -2176,8 +2176,8 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( &mut oracle_map, )?; - if user.is_being_liquidated() { - user.exit_liquidation(); + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); } if user.is_isolated_position_being_liquidated(perp_market_index)? { @@ -2239,7 +2239,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( user.exit_isolated_position_liquidation(perp_market_index)?; } - 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( user, @@ -2250,7 +2250,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } } } @@ -2300,7 +2300,7 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; { let perp_market = &perp_market_map.get_ref(&perp_market_index)?; @@ -3535,7 +3535,7 @@ pub fn handle_update_user_reduce_only( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_reduce_only_status(reduce_only)?; Ok(()) @@ -3548,7 +3548,7 @@ pub fn handle_update_user_advanced_lp( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_advanced_lp_status(advanced_lp)?; Ok(()) @@ -3561,7 +3561,7 @@ pub fn handle_update_user_protected_maker_orders( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; validate!( protected_maker_orders != user.is_protected_maker(), @@ -3785,7 +3785,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( let mut user = load_mut!(&ctx.accounts.user)?; let delegate_is_signer = user.delegate == ctx.accounts.authority.key(); - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; math::liquidation::validate_user_not_being_liquidated( &mut user, diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index e60f6a60a1..e0c28ebfa6 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -225,7 +225,7 @@ pub fn validate_user_not_being_liquidated( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, ) -> DriftResult { - if !user.is_being_liquidated() && !user.any_isolated_position_being_liquidated() { + if !user.is_being_liquidated() { return Ok(()); } @@ -237,9 +237,9 @@ pub fn validate_user_not_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if user.is_being_liquidated() { + if user.is_cross_margin_being_liquidated() { if margin_calculation.cross_margin_can_exit_liquidation()? { - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); } else { return Err(ErrorCode::UserIsBeingLiquidated); } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 28cdb854ec..bb3c2c54d2 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -69,7 +69,7 @@ impl CrossMarginLiquidatePerpMode { impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult { - Ok(user.is_being_liquidated()) + Ok(user.is_cross_margin_being_liquidated()) } fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { @@ -81,7 +81,7 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { - Ok(user.exit_liquidation()) + Ok(user.exit_cross_margin_liquidation()) } fn get_cancel_orders_params(&self) -> (Option, Option, bool) { @@ -118,11 +118,11 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - Ok(user.enter_bankruptcy()) + Ok(user.enter_cross_margin_bankruptcy()) } fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - Ok(user.exit_bankruptcy()) + Ok(user.exit_cross_margin_bankruptcy()) } fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 879e50997b..4db918b828 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -646,4 +646,8 @@ impl MarginCalculation { Err(ErrorCode::InvalidMarginCalculation) } } + + pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { + self.isolated_position_margin_calculation.contains_key(&market_index) + } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 2c65ad60b3..96ecd127bf 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -135,10 +135,14 @@ pub struct User { impl User { pub fn is_being_liquidated(&self) -> bool { + self.is_cross_margin_being_liquidated() || self.has_isolated_position_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 { + pub fn is_cross_margin_bankrupt(&self) -> bool { self.status & (UserStatus::Bankrupt as u8) > 0 } @@ -369,8 +373,8 @@ 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); } @@ -379,7 +383,7 @@ impl User { self.last_active_slot = slot; - let liquidation_id = if self.any_isolated_position_being_liquidated() { + let liquidation_id = if self.has_isolated_position_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -388,24 +392,24 @@ impl User { 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 any_isolated_position_being_liquidated(&self) -> bool { + pub fn has_isolated_position_being_liquidated(&self) -> bool { self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) } @@ -414,7 +418,7 @@ impl User { return self.next_liquidation_id.safe_sub(1); } - let liquidation_id = if self.is_being_liquidated() || self.any_isolated_position_being_liquidated() { + let liquidation_id = if self.is_cross_margin_being_liquidated() || self.has_isolated_position_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -462,7 +466,7 @@ impl User { } pub fn update_last_active_slot(&mut self, slot: u64) { - if !self.is_being_liquidated() { + if !self.is_cross_margin_being_liquidated() { self.last_active_slot = slot; } self.idle = false; diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 94312c9e63..09484892ab 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -1671,36 +1671,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); } } @@ -2332,7 +2332,7 @@ mod next_liquidation_id { }; user.perp_positions[1] = isolated_position_2; - let liquidation_id = user.enter_liquidation(1).unwrap(); + let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 1); let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); @@ -2340,7 +2340,7 @@ mod next_liquidation_id { user.exit_isolated_position_liquidation(1).unwrap(); - user.exit_liquidation(); + user.exit_cross_margin_liquidation(); let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); @@ -2348,7 +2348,7 @@ mod next_liquidation_id { let liquidation_id = user.enter_isolated_position_liquidation(2).unwrap(); assert_eq!(liquidation_id, 2); - let liquidation_id = user.enter_liquidation(1).unwrap(); + let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); } } \ No newline at end of file diff --git a/programs/drift/src/validation/user.rs b/programs/drift/src/validation/user.rs index 3f527fed0f..f19851b35f 100644 --- a/programs/drift/src/validation/user.rs +++ b/programs/drift/src/validation/user.rs @@ -17,7 +17,7 @@ pub fn validate_user_deletion( )?; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserCantBeDeleted, "user bankrupt" )?; @@ -87,7 +87,7 @@ pub fn validate_user_is_idle(user: &User, slot: u64, accelerated: bool) -> Drift )?; validate!( - !user.is_bankrupt(), + !user.is_cross_margin_bankrupt(), ErrorCode::UserNotInactive, "user bankrupt" )?; From 26960c8a7e4be44a55f5ab1dc108c28b41cbabbd Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 16:46:18 -0400 Subject: [PATCH 033/159] start adding test --- programs/drift/src/controller/amm/tests.rs | 1 - programs/drift/src/controller/liquidation.rs | 110 ++- .../drift/src/controller/liquidation/tests.rs | 901 ++++++++++++++++++ programs/drift/src/controller/orders.rs | 54 +- programs/drift/src/controller/pnl.rs | 5 +- .../drift/src/controller/pnl/delisting.rs | 2 +- programs/drift/src/controller/position.rs | 14 +- .../drift/src/controller/position/tests.rs | 5 +- programs/drift/src/instructions/keeper.rs | 5 +- programs/drift/src/instructions/user.rs | 39 +- programs/drift/src/lib.rs | 21 +- programs/drift/src/math/bankruptcy.rs | 6 +- programs/drift/src/math/cp_curve/tests.rs | 2 +- programs/drift/src/math/funding.rs | 4 +- programs/drift/src/math/liquidation.rs | 22 +- programs/drift/src/math/margin.rs | 37 +- programs/drift/src/math/orders.rs | 8 +- programs/drift/src/math/position.rs | 5 +- programs/drift/src/state/liquidation_mode.rs | 116 ++- .../drift/src/state/margin_calculation.rs | 106 ++- programs/drift/src/state/user.rs | 54 +- programs/drift/src/state/user/tests.rs | 2 +- 22 files changed, 1324 insertions(+), 195 deletions(-) diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index 5031a75bbc..a2a33fd6d5 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -255,7 +255,6 @@ fn iterative_no_bounds_formualic_k_tests() { assert_eq!(market.amm.total_fee_minus_distributions, 985625029); } - #[test] fn update_pool_balances_test_high_util_borrow() { let mut market = PerpMarket { diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 05cc104eca..07d2ee1203 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1,7 +1,9 @@ use std::ops::{Deref, DerefMut}; use crate::msg; -use crate::state::liquidation_mode::{get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode}; +use crate::state::liquidation_mode::{ + get_perp_liquidation_mode, CrossMarginLiquidatePerpMode, LiquidatePerpMode, +}; use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; @@ -96,7 +98,7 @@ 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); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; validate!( !liquidation_mode.is_user_bankrupt(&user)?, @@ -152,10 +154,14 @@ pub fn liquidate_perp( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { + 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 && liquidation_mode.can_exit_liquidation(&margin_calculation)? { + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -188,7 +194,8 @@ pub fn liquidate_perp( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = liquidation_mode.get_cancel_orders_params(); + let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -238,7 +245,8 @@ pub fn liquidate_perp( )?; let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; - let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -246,7 +254,8 @@ pub fn liquidate_perp( liquidation_mode.increment_free_margin(user, margin_freed)?; if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -679,7 +688,8 @@ pub fn liquidate_perp( }; emit!(fill_record); - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -736,7 +746,7 @@ 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); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; validate!( !liquidation_mode.is_user_bankrupt(&user)?, @@ -792,10 +802,14 @@ pub fn liquidate_perp_with_fill( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { + 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 && liquidation_mode.can_exit_liquidation(&margin_calculation)? { + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { liquidation_mode.exit_liquidation(&mut user)?; return Ok(()); } @@ -817,8 +831,9 @@ pub fn liquidate_perp_with_fill( || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - - let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); + + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( &mut user, user_key, @@ -868,7 +883,8 @@ pub fn liquidate_perp_with_fill( )?; let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; - let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) @@ -876,7 +892,8 @@ pub fn liquidate_perp_with_fill( liquidation_mode.increment_free_margin(&mut user, margin_freed); if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1129,7 +1146,8 @@ pub fn liquidate_perp_with_fill( existing_direction, )?; - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1397,7 +1415,9 @@ pub fn liquidate_spot( if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.cross_margin_can_exit_liquidation()? + { user.exit_cross_margin_liquidation(); return Ok(()); } @@ -1928,7 +1948,9 @@ pub fn liquidate_spot_with_swap_begin( if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.cross_margin_can_exit_liquidation()? + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); } @@ -1948,7 +1970,7 @@ pub fn liquidate_spot_with_swap_begin( None, None, None, - true + true, )?; // check if user exited liquidation territory @@ -2501,7 +2523,9 @@ pub fn liquidate_borrow_for_perp_pnl( if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); - } else if user.is_cross_margin_being_liquidated() && margin_calculation.cross_margin_can_exit_liquidation()? { + } else if user.is_cross_margin_being_liquidated() + && margin_calculation.cross_margin_can_exit_liquidation()? + { user.exit_cross_margin_liquidation(); return Ok(()); } @@ -2522,7 +2546,7 @@ pub fn liquidate_borrow_for_perp_pnl( None, None, None, - true + true, )?; // check if user exited liquidation territory @@ -2788,7 +2812,7 @@ 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); + let liquidation_mode = get_perp_liquidation_mode(&user, perp_market_index)?; validate!( !liquidation_mode.is_user_bankrupt(&user)?, @@ -2962,10 +2986,14 @@ pub fn liquidate_perp_pnl_for_deposit( )?; let user_is_being_liquidated = liquidation_mode.user_is_being_liquidated(&user)?; - if !user_is_being_liquidated && liquidation_mode.meets_margin_requirements(&margin_calculation)? { + 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 && liquidation_mode.can_exit_liquidation(&margin_calculation)? { + } else if user_is_being_liquidated + && liquidation_mode.can_exit_liquidation(&margin_calculation)? + { liquidation_mode.exit_liquidation(user)?; return Ok(()); } @@ -2973,7 +3001,8 @@ pub fn liquidate_perp_pnl_for_deposit( let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; - let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = liquidation_mode.get_cancel_orders_params(); + let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = + liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, user_key, @@ -2990,8 +3019,8 @@ pub fn liquidate_perp_pnl_for_deposit( cancel_orders_is_isolated, )?; - let (safest_tier_spot_liability, safest_tier_perp_liability) = - liquidation_mode.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)); @@ -3007,20 +3036,23 @@ pub fn liquidate_perp_pnl_for_deposit( )?; let initial_margin_shortage = liquidation_mode.margin_shortage(&margin_calculation)?; - let new_margin_shortage = liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; + let new_margin_shortage = + liquidation_mode.margin_shortage(&intermediate_margin_calculation)?; margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; liquidation_mode.increment_free_margin(user, margin_freed); - let exiting_liq_territory = liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)?; + 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(&intermediate_margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3228,7 +3260,8 @@ 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)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3266,9 +3299,11 @@ pub fn resolve_perp_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - let liquidation_mode = get_perp_liquidation_mode(&user, market_index); + let liquidation_mode = get_perp_liquidation_mode(&user, market_index)?; - if !liquidation_mode.is_user_bankrupt(&user)? && liquidation_mode.should_user_enter_bankruptcy(&user)? { + if !liquidation_mode.is_user_bankrupt(&user)? + && liquidation_mode.should_user_enter_bankruptcy(&user)? + { liquidation_mode.enter_bankruptcy(user)?; } @@ -3454,7 +3489,8 @@ pub fn resolve_perp_bankruptcy( let liquidation_id = user.next_liquidation_id.safe_sub(1)?; - let (margin_requirement, total_collateral, bit_flags) = liquidation_mode.get_event_fields(&margin_calculation)?; + let (margin_requirement, total_collateral, bit_flags) = + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3698,7 +3734,11 @@ pub fn set_user_status_to_being_liquidated( user.enter_cross_margin_liquidation(slot)?; } - let isolated_position_market_indexes = user.perp_positions.iter().filter_map(|position| position.is_isolated().then_some(position.market_index)).collect::>(); + let isolated_position_market_indexes = user + .perp_positions + .iter() + .filter_map(|position| position.is_isolated().then_some(position.market_index)) + .collect::>(); // for market_index in isolated_position_market_indexes { // let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index cc815592f3..4548cbbb33 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -8910,3 +8910,904 @@ 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::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::liquidate_perp; + 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_user_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_position_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_isolated_position_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_position_being_liquidated(0).unwrap()); + assert_eq!(market_after.amm.total_liquidation_fee, 41787043); + } +} \ No newline at end of file diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index ca09018b8c..f30722744e 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -11,8 +11,7 @@ use crate::controller::funding::settle_funding_payment; use crate::controller::position; use crate::controller::position::{ add_new_position, decrease_open_bids_and_asks, get_position_index, increase_open_bids_and_asks, - update_position_and_market, update_quote_asset_amount, - PositionDirection, + update_position_and_market, update_quote_asset_amount, PositionDirection, }; use crate::controller::spot_balance::{ update_spot_balances, update_spot_market_cumulative_interest, @@ -522,7 +521,12 @@ pub fn cancel_orders( 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::>(); + 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; @@ -536,7 +540,9 @@ 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) { + } else if skip_isolated_positions + && isolated_position_market_indexes.contains(&user.orders[order_index].market_index) + { continue; } @@ -1945,11 +1951,20 @@ fn fulfill_perp_order( )?; if !taker_margin_calculation.meets_margin_requirement() { - let (margin_requirement, total_collateral) = if taker_margin_calculation.has_isolated_position_margin_calculation(market_index) { - let isolated_position_margin_calculation = taker_margin_calculation.get_isolated_position_margin_calculation(market_index)?; - (isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral) + let (margin_requirement, total_collateral) = if taker_margin_calculation + .has_isolated_position_margin_calculation(market_index) + { + let isolated_position_margin_calculation = taker_margin_calculation + .get_isolated_position_margin_calculation(market_index)?; + ( + isolated_position_margin_calculation.margin_requirement, + isolated_position_margin_calculation.total_collateral, + ) } else { - (taker_margin_calculation.margin_requirement, taker_margin_calculation.total_collateral) + ( + taker_margin_calculation.margin_requirement, + taker_margin_calculation.total_collateral, + ) }; msg!( @@ -2012,11 +2027,20 @@ fn fulfill_perp_order( } if !maker_margin_calculation.meets_margin_requirement() { - let (margin_requirement, total_collateral) = if maker_margin_calculation.has_isolated_position_margin_calculation(market_index) { - let isolated_position_margin_calculation = maker_margin_calculation.get_isolated_position_margin_calculation(market_index)?; - (isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral) + let (margin_requirement, total_collateral) = if maker_margin_calculation + .has_isolated_position_margin_calculation(market_index) + { + let isolated_position_margin_calculation = maker_margin_calculation + .get_isolated_position_margin_calculation(market_index)?; + ( + isolated_position_margin_calculation.margin_requirement, + isolated_position_margin_calculation.total_collateral, + ) } else { - (maker_margin_calculation.margin_requirement, maker_margin_calculation.total_collateral) + ( + maker_margin_calculation.margin_requirement, + maker_margin_calculation.total_collateral, + ) }; msg!( @@ -3202,7 +3226,8 @@ pub fn force_cancel_orders( ErrorCode::SufficientCollateral )?; - let cross_margin_meets_initial_margin_requirement = margin_calc.cross_margin_meets_margin_requirement(); + let cross_margin_meets_initial_margin_requirement = + margin_calc.cross_margin_meets_margin_requirement(); let mut total_fee = 0_u64; @@ -3253,7 +3278,8 @@ pub fn force_cancel_orders( continue; } } else { - let isolated_position_meets_margin_requirement = margin_calc.isolated_position_meets_margin_requirement(market_index)?; + let isolated_position_meets_margin_requirement = + margin_calc.isolated_position_meets_margin_requirement(market_index)?; if isolated_position_meets_margin_requirement { continue; } diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index ea6df2ed68..14e4fbd865 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -1,9 +1,6 @@ use crate::controller::amm::{update_pnl_pool_and_user_balance, update_pool_balances}; use crate::controller::funding::settle_funding_payment; -use crate::controller::orders::{ - cancel_orders, - validate_market_within_price_band, -}; +use crate::controller::orders::{cancel_orders, validate_market_within_price_band}; use crate::controller::position::{ get_position_index, update_position_and_market, update_quote_asset_amount, update_quote_asset_and_break_even_amount, update_settled_pnl, PositionDelta, diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index f781b93d1e..70f81a716b 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2601,7 +2601,7 @@ pub mod delisting_test { &strict_quote_price, MarginRequirementType::Initial, 0, - false + false, ) .unwrap(); diff --git a/programs/drift/src/controller/position.rs b/programs/drift/src/controller/position.rs index fea9ef135a..30150d3c6e 100644 --- a/programs/drift/src/controller/position.rs +++ b/programs/drift/src/controller/position.rs @@ -95,10 +95,8 @@ pub fn update_position_and_market( let update_type = get_position_update_type(position, delta)?; // Update User - let ( - new_base_asset_amount, - new_quote_asset_amount, - ) = get_new_position_amounts(position, delta)?; + let (new_base_asset_amount, new_quote_asset_amount) = + get_new_position_amounts(position, delta)?; let (new_quote_entry_amount, new_quote_break_even_amount, pnl) = match update_type { PositionUpdateType::Open | PositionUpdateType::Increase => { @@ -475,9 +473,7 @@ pub fn update_quote_asset_amount( return Ok(()); } - if position.quote_asset_amount == 0 - && position.base_asset_amount == 0 - { + if position.quote_asset_amount == 0 && position.base_asset_amount == 0 { market.number_of_users = market.number_of_users.safe_add(1)?; } @@ -485,9 +481,7 @@ pub fn update_quote_asset_amount( market.amm.quote_asset_amount = market.amm.quote_asset_amount.safe_add(delta.cast()?)?; - if position.quote_asset_amount == 0 - && position.base_asset_amount == 0 - { + if position.quote_asset_amount == 0 && position.base_asset_amount == 0 { market.number_of_users = market.number_of_users.saturating_sub(1); } diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index bfe693fe44..7c41bf2591 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -1,9 +1,7 @@ use crate::controller::amm::{ calculate_base_swap_output_with_spread, move_price, recenter_perp_market_amm, swap_base_asset, }; -use crate::controller::position::{ - update_position_and_market, PositionDelta, -}; +use crate::controller::position::{update_position_and_market, PositionDelta}; use crate::controller::repeg::_update_amm; use crate::math::amm::calculate_market_open_bids_asks; @@ -41,7 +39,6 @@ use anchor_lang::Owner; use solana_program::pubkey::Pubkey; use std::str::FromStr; - #[test] fn amm_pool_balance_liq_fees_example() { let perp_market_str = String::from("Ct8MLGv1N/dquEe6RHLCjPXRFs689/VXwfnq/aHEADtX6J/C8GaZXDKZ6iACt2rxmu8p8Fh+gR3ERNNiw5jAdKhvts0jU4yP8/YGAAAAAAAAAAAAAAAAAAEAAAAAAAAAYOoGAAAAAAD08AYAAAAAAFDQ0WcAAAAAU20cou///////////////zqG0jcAAAAAAAAAAAAAAACyy62lmssEAAAAAAAAAAAAAAAAAAAAAACuEBLjOOAUAAAAAAAAAAAAiQqZJDPTFAAAAAAAAAAAANiFEAAAAAAAAAAAAAAAAABEI0dQmUcTAAAAAAAAAAAAxIkaBDObFgAAAAAAAAAAAD4fkf+02RQAAAAAAAAAAABN+wYAAAAAAAAAAAAAAAAAy1BRbfXSFAAAAAAAAAAAAADOOHkhTQcAAAAAAAAAAAAAFBriILP4////////////SMyW3j0AAAAAAAAAAAAAALgVvHwEAAAAAAAAAAAAAAAAADQm9WscAAAAAAAAAAAAURkvFjoAAAAAAAAAAAAAAHIxjo/f/f/////////////TuoG31QEAAAAAAAAAAAAAP8QC+7L9/////////////3SO4oj1AQAAAAAAAAAAAAAAgFcGo5wAAAAAAAAAAAAAzxUAAAAAAADPFQAAAAAAAM8VAAAAAAAAPQwAAAAAAABk1DIXBgEAAAAAAAAAAAAAKqQCt7MAAAAAAAAAAAAAAP0Q55dSAAAAAAAAAAAAAACS+qA0KQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALB5hg2UAAAAAAAAAAAAAAAnMANRAAAAAAAAAAAAAAAAmdj/UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+LAqY3t8UAAAAAAAAAAAAhk/TOI3TFAAAAAAAAAAAAG1uRreN4BQAAAAAAAAAAABkKKeG3tIUAAAAAAAAAAAA8/YGAAAAAAD+/////////2DqBgAAAAAA5OoGAAAAAACi6gYAAAAAAKzxBgAAAAAAMj1zEwAAAABIAgAAAAAAAIy24v//////tMvRZwAAAAAQDgAAAAAAAADKmjsAAAAAZAAAAAAAAAAA8gUqAQAAAAAAAAAAAAAAs3+BskEAAADIfXYRAAAAAIIeqQIAAAAAdb7RZwAAAABxDAAAAAAAAJMMAAAAAAAAUNDRZwAAAAD6AAAA1DAAAIQAAAB9AAAAfgAAAAAAAABkADIAZGQMAQAAAAADAAAAX79DBQAAAABIC9oEAwAAAK3TwZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFdJRi1QRVJQICAgICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADd4BgAAAAAAlCUAAAAAAAAcCgAAAAAAAGQAAABkAAAAqGEAAFDDAADECQAA4gQAAAAAAAAQJwAA2QAAAIgBAAAXAAEAAwAAAAAAAAEBAOgD9AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 2591439bfd..101ccfb84e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2708,14 +2708,13 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( let custom_margin_ratio_before = user.max_margin_ratio; user.max_margin_ratio = 0; - let margin_buffer= MARGIN_PRECISION / 100; // 1% buffer + let margin_buffer = MARGIN_PRECISION / 100; // 1% buffer let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(margin_buffer), + MarginContext::standard(MarginRequirementType::Initial).margin_buffer(margin_buffer), )?; let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 2fa326edc1..82eee452b1 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1950,7 +1950,6 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( 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())?; @@ -2169,12 +2168,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( now, )?; - validate_spot_margin_trading( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - )?; + validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; if user.is_cross_margin_being_liquidated() { user.exit_cross_margin_liquidation(); @@ -2190,7 +2184,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( perp_market_index, state.liquidation_margin_buffer_ratio, )?; - + if !is_being_liquidated { user.exit_isolated_position_liquidation(perp_market_index)?; } @@ -2198,7 +2192,9 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( } 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_position_token_amount(&spot_market)?; + let isolated_perp_position_token_amount = user + .force_get_isolated_perp_position_mut(perp_market_index)? + .get_isolated_position_token_amount(&spot_market)?; validate!( amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, @@ -2248,15 +2244,13 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( &mut oracle_map, state.liquidation_margin_buffer_ratio, )?; - + if !is_being_liquidated { user.exit_cross_margin_liquidation(); } } } - - user.update_last_active_slot(slot); let spot_market = spot_market_map.get_ref(&spot_market_index)?; @@ -2328,9 +2322,11 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( spot_market.get_precision().cast()?, )?; - let isolated_perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + let isolated_perp_position = + user.force_get_isolated_perp_position_mut(perp_market_index)?; - let isolated_position_token_amount = isolated_perp_position.get_isolated_position_token_amount(spot_market)?; + let isolated_position_token_amount = + isolated_perp_position.get_isolated_position_token_amount(spot_market)?; validate!( amount as u128 <= isolated_position_token_amount, @@ -3535,7 +3531,10 @@ pub fn handle_update_user_reduce_only( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!( + !user.is_cross_margin_being_liquidated(), + ErrorCode::LiquidationsOngoing + )?; user.update_reduce_only_status(reduce_only)?; Ok(()) @@ -3548,7 +3547,10 @@ pub fn handle_update_user_advanced_lp( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!( + !user.is_cross_margin_being_liquidated(), + ErrorCode::LiquidationsOngoing + )?; user.update_advanced_lp_status(advanced_lp)?; Ok(()) @@ -3561,7 +3563,10 @@ pub fn handle_update_user_protected_maker_orders( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!(!user.is_cross_margin_being_liquidated(), ErrorCode::LiquidationsOngoing)?; + validate!( + !user.is_cross_margin_being_liquidated(), + ErrorCode::LiquidationsOngoing + )?; validate!( protected_maker_orders != user.is_protected_maker(), diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 5f172e3043..7edf12a991 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -174,7 +174,12 @@ pub mod drift { perp_market_index: u16, amount: u64, ) -> Result<()> { - handle_deposit_into_isolated_perp_position(ctx, spot_market_index, perp_market_index, amount) + handle_deposit_into_isolated_perp_position( + ctx, + spot_market_index, + perp_market_index, + amount, + ) } pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( @@ -183,7 +188,12 @@ pub mod drift { perp_market_index: u16, amount: i64, ) -> Result<()> { - handle_transfer_isolated_perp_position_deposit(ctx, spot_market_index, perp_market_index, amount) + handle_transfer_isolated_perp_position_deposit( + ctx, + spot_market_index, + perp_market_index, + amount, + ) } pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( @@ -192,7 +202,12 @@ pub mod drift { perp_market_index: u16, amount: u64, ) -> Result<()> { - handle_withdraw_from_isolated_perp_position(ctx, spot_market_index, perp_market_index, amount) + handle_withdraw_from_isolated_perp_position( + ctx, + spot_market_index, + perp_market_index, + amount, + ) } pub fn place_perp_order<'c: 'info, 'info>( diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 7defdea6b3..f8963c61c4 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -42,5 +42,7 @@ pub fn is_user_isolated_position_bankrupt(user: &User, market_index: u16) -> Dri return Ok(false); } - return Ok(perp_position.base_asset_amount == 0 && perp_position.quote_asset_amount < 0 && !perp_position.has_open_order()); -} \ No newline at end of file + 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/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index 2ae3d2506b..f9100c98cf 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -365,4 +365,4 @@ fn amm_spread_adj_logic() { update_spreads(&mut market, reserve_price).unwrap(); assert_eq!(market.amm.long_spread, 110); assert_eq!(market.amm.short_spread, 110); -} \ No newline at end of file +} diff --git a/programs/drift/src/math/funding.rs b/programs/drift/src/math/funding.rs index 683f62c2cc..c723cb37bb 100644 --- a/programs/drift/src/math/funding.rs +++ b/programs/drift/src/math/funding.rs @@ -27,9 +27,7 @@ pub fn calculate_funding_rate_long_short( ) -> DriftResult<(i128, i128, i128)> { // Calculate the funding payment owed by the net_market_position if funding is not capped // If the net market position owes funding payment, the protocol receives payment - let settled_net_market_position = market - .amm - .base_asset_amount_with_amm; + let settled_net_market_position = market.amm.base_asset_amount_with_amm; let net_market_position_funding_payment = calculate_funding_payment_in_quote_precision(funding_rate, settled_net_market_position)?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index e0c28ebfa6..9f25648c4e 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -244,13 +244,20 @@ pub fn validate_user_not_being_liquidated( return Err(ErrorCode::UserIsBeingLiquidated); } } else { - let isolated_positions_being_liquidated = user.perp_positions.iter().filter(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()).map(|position| position.market_index).collect::>(); + let isolated_positions_being_liquidated = user + .perp_positions + .iter() + .filter(|position| { + position.is_isolated() && position.is_isolated_position_being_liquidated() + }) + .map(|position| position.market_index) + .collect::>(); for perp_market_index in isolated_positions_being_liquidated { - if margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)? { - user.exit_isolated_position_liquidation(perp_market_index)?; - } else { - return Err(ErrorCode::UserIsBeingLiquidated); - } + if margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)? { + user.exit_isolated_position_liquidation(perp_market_index)?; + } else { + return Err(ErrorCode::UserIsBeingLiquidated); + } } } @@ -274,7 +281,8 @@ pub fn is_isolated_position_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; + let is_being_liquidated = + !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; Ok(is_being_liquidated) } diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 6e12af8ca4..16f75868ae 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -525,20 +525,16 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0, )?; - 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, - user_high_leverage_mode, - )?; + 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, + user_high_leverage_mode, + )?; calculation.update_fuel_perp_bonus( market, @@ -556,7 +552,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( "e_spot_market, &SpotBalanceType::Deposit, )?; - + let quote_token_value = get_strict_token_value( quote_token_amount.cast::()?, quote_spot_market.decimals, @@ -579,7 +575,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_liability_value, MarketIdentifier::perp(market.market_index), )?; - + calculation.add_total_collateral(weighted_pnl)?; } @@ -948,9 +944,14 @@ pub fn calculate_user_equity( is_oracle_valid_for_action(quote_oracle_validity, Some(DriftAction::MarginCalc))?; if market_position.is_isolated() { - let quote_token_amount = market_position.get_isolated_position_token_amount("e_spot_market)?; + let quote_token_amount = + market_position.get_isolated_position_token_amount("e_spot_market)?; - let token_value = get_token_value(quote_token_amount.cast()?, quote_spot_market.decimals, quote_oracle_price_data.price)?; + 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)?; } diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 544480be2e..85e2928959 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -860,9 +860,13 @@ pub fn calculate_max_perp_order_size( let is_isolated_position = user.perp_positions[position_index].is_isolated(); let free_collateral_before = if is_isolated_position { - margin_calculation.get_isolated_position_free_collateral(market_index)?.cast::()? + margin_calculation + .get_isolated_position_free_collateral(market_index)? + .cast::()? } else { - margin_calculation.get_cross_margin_free_collateral()?.cast::()? + margin_calculation + .get_cross_margin_free_collateral()? + .cast::()? }; let perp_market = perp_market_map.get_ref(&market_index)?; diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index 8f008c0f35..998970480d 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -199,8 +199,5 @@ pub fn get_new_position_amounts( .base_asset_amount .safe_add(delta.base_asset_amount)?; - Ok(( - new_base_asset_amount, - new_quote_asset_amount, - )) + Ok((new_base_asset_amount, new_quote_asset_amount)) } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index bb3c2c54d2..662db067ee 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -1,13 +1,37 @@ use solana_program::msg; -use crate::{controller::{spot_balance::update_spot_balances, spot_position::update_spot_balances_and_cumulative_deposits}, error::{DriftResult, ErrorCode}, math::{bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap}, state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX}; - -use super::{events::LiquidationBitFlag, perp_market::ContractTier, perp_market_map::PerpMarketMap, spot_market::{AssetTier, SpotBalanceType, SpotMarket}, spot_market_map::SpotMarketMap, user::{MarketType, User}}; +use crate::{ + controller::{ + spot_balance::update_spot_balances, + spot_position::update_spot_balances_and_cumulative_deposits, + }, + error::{DriftResult, ErrorCode}, + math::{ + bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, + liquidation::calculate_max_pct_to_liquidate, + margin::calculate_user_safest_position_tiers, + safe_unwrap::SafeUnwrap, + }, + state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, + validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX, +}; + +use super::{ + events::LiquidationBitFlag, + perp_market::ContractTier, + perp_market_map::PerpMarketMap, + spot_market::{AssetTier, SpotBalanceType, SpotMarket}, + spot_market_map::SpotMarketMap, + user::{MarketType, User}, +}; pub trait LiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult; - fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult; + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult; fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult; @@ -34,13 +58,21 @@ pub trait LiquidatePerpMode { fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()>; - fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)>; + 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 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, @@ -53,8 +85,18 @@ pub trait LiquidatePerpMode { fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult; } -pub fn get_perp_liquidation_mode(user: &User, market_index: u16) -> Box { - Box::new(CrossMarginLiquidatePerpMode::new(market_index)) +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(IsolatedLiquidatePerpMode::new(market_index)) + } else { + Box::new(CrossMarginLiquidatePerpMode::new(market_index)) + }; + + Ok(mode) } pub struct CrossMarginLiquidatePerpMode { @@ -72,7 +114,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.is_cross_margin_being_liquidated()) } - fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult { Ok(margin_calculation.cross_margin_meets_margin_requirement()) } @@ -125,8 +170,15 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { 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 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<()> { @@ -163,7 +215,12 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(token_amount) } - fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)> { + 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) } @@ -208,7 +265,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.is_isolated_position_being_liquidated(self.market_index) } - fn meets_margin_requirements(&self, margin_calculation: &MarginCalculation) -> DriftResult { + fn meets_margin_requirements( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult { margin_calculation.isolated_position_meets_margin_requirement(self.market_index) } @@ -255,9 +315,19 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.exit_isolated_position_bankruptcy(self.market_index) } - fn get_event_fields(&self, margin_calculation: &MarginCalculation) -> DriftResult<(u128, i128, u8)> { - let isolated_position_margin_calculation = margin_calculation.isolated_position_margin_calculation.get(&self.market_index).safe_unwrap()?; - Ok((isolated_position_margin_calculation.margin_requirement, isolated_position_margin_calculation.total_collateral, LiquidationBitFlag::IsolatedPosition as u8)) + fn get_event_fields( + &self, + margin_calculation: &MarginCalculation, + ) -> DriftResult<(u128, i128, u8)> { + let isolated_position_margin_calculation = margin_calculation + .isolated_position_margin_calculation + .get(&self.market_index) + .safe_unwrap()?; + Ok(( + isolated_position_margin_calculation.margin_requirement, + isolated_position_margin_calculation.total_collateral, + LiquidationBitFlag::IsolatedPosition as u8, + )) } fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { @@ -270,8 +340,9 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { 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_position_token_amount(spot_market)?; + + let token_amount = + isolated_perp_position.get_isolated_position_token_amount(spot_market)?; validate!( token_amount != 0, @@ -283,7 +354,12 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { Ok(token_amount) } - fn calculate_user_safest_position_tiers(&self, user: &User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap) -> DriftResult<(AssetTier, ContractTier)> { + 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)) @@ -312,4 +388,4 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { margin_calculation.isolated_position_margin_shortage(self.market_index) } -} \ No newline at end of file +} diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 4db918b828..b55e11fed3 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -198,9 +198,9 @@ pub struct IsolatedPositionMarginCalculation { } impl IsolatedPositionMarginCalculation { - pub fn get_total_collateral_plus_buffer(&self) -> i128 { - self.total_collateral.saturating_add(self.total_collateral_buffer) + self.total_collateral + .saturating_add(self.total_collateral_buffer) } pub fn meets_margin_requirement(&self) -> bool { @@ -212,7 +212,11 @@ impl IsolatedPositionMarginCalculation { } pub fn margin_shortage(&self) -> DriftResult { - Ok(self.margin_requirement_plus_buffer.cast::()?.safe_sub(self.get_total_collateral_plus_buffer())?.unsigned_abs()) + Ok(self + .margin_requirement_plus_buffer + .cast::()? + .safe_sub(self.get_total_collateral_plus_buffer())? + .unsigned_abs()) } } @@ -283,9 +287,16 @@ impl MarginCalculation { Ok(()) } - pub fn add_isolated_position_margin_calculation(&mut self, market_index: u16, deposit_value: i128, pnl: i128, liability_value: u128, margin_requirement: u128) -> DriftResult { + pub fn add_isolated_position_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 { @@ -293,7 +304,9 @@ impl MarginCalculation { }; 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)? + margin_requirement.safe_add( + liability_value.safe_mul(self.context.margin_buffer)? / MARGIN_PRECISION_U128, + )? } else { 0 }; @@ -305,13 +318,14 @@ impl MarginCalculation { margin_requirement_plus_buffer, }; - self.isolated_position_margin_calculation.insert(market_index, isolated_position_margin_calculation); + self.isolated_position_margin_calculation + .insert(market_index, isolated_position_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_plus_buffer)?; + .safe_add(margin_requirement)?; } } @@ -402,7 +416,8 @@ impl MarginCalculation { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + { if !isolated_position_margin_calculation.meets_margin_requirement() { return false; } @@ -412,18 +427,20 @@ impl MarginCalculation { } pub fn meets_margin_requirement_with_buffer(&self) -> bool { - let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement_with_buffer(); + let cross_margin_meets_margin_requirement = + self.cross_margin_meets_margin_requirement_with_buffer(); if !cross_margin_meets_margin_requirement { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation { + for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { return false; } } - + true } @@ -438,13 +455,27 @@ impl MarginCalculation { } #[inline(always)] - pub fn isolated_position_meets_margin_requirement(&self, market_index: u16) -> DriftResult { - Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement()) + pub fn isolated_position_meets_margin_requirement( + &self, + market_index: u16, + ) -> DriftResult { + Ok(self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement()) } #[inline(always)] - pub fn isolated_position_meets_margin_requirement_with_buffer(&self, market_index: u16) -> DriftResult { - Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) + pub fn isolated_position_meets_margin_requirement_with_buffer( + &self, + market_index: u16, + ) -> DriftResult { + Ok(self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement_with_buffer()) } pub fn cross_margin_can_exit_liquidation(&self) -> DriftResult { @@ -461,8 +492,12 @@ impl MarginCalculation { msg!("liquidation mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); } - - Ok(self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.meets_margin_requirement_with_buffer()) + + Ok(self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .meets_margin_requirement_with_buffer()) } pub fn cross_margin_margin_shortage(&self) -> DriftResult { @@ -484,7 +519,10 @@ impl MarginCalculation { return Err(ErrorCode::InvalidMarginCalculation); } - self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?.margin_shortage() + self.isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()? + .margin_shortage() } pub fn tracked_market_margin_shortage(&self, margin_shortage: u128) -> DriftResult { @@ -504,9 +542,7 @@ impl MarginCalculation { Some(isolated_position_margin_calculation) => { isolated_position_margin_calculation.margin_requirement } - None => { - self.margin_requirement - } + None => self.margin_requirement, } } else { self.margin_requirement @@ -529,9 +565,17 @@ impl MarginCalculation { } pub fn get_isolated_position_free_collateral(&self, market_index: u16) -> DriftResult { - let isolated_position_margin_calculation = self.isolated_position_margin_calculation.get(&market_index).safe_unwrap()?; - isolated_position_margin_calculation.total_collateral - .safe_sub(isolated_position_margin_calculation.margin_requirement.cast::()?)? + let isolated_position_margin_calculation = self + .isolated_position_margin_calculation + .get(&market_index) + .safe_unwrap()?; + isolated_position_margin_calculation + .total_collateral + .safe_sub( + isolated_position_margin_calculation + .margin_requirement + .cast::()?, + )? .max(0) .cast() } @@ -639,8 +683,13 @@ impl MarginCalculation { Ok(()) } - pub fn get_isolated_position_margin_calculation(&self, market_index: u16) -> DriftResult<&IsolatedPositionMarginCalculation> { - if let Some(isolated_position_margin_calculation) = self.isolated_position_margin_calculation.get(&market_index) { + pub fn get_isolated_position_margin_calculation( + &self, + market_index: u16, + ) -> DriftResult<&IsolatedPositionMarginCalculation> { + if let Some(isolated_position_margin_calculation) = + self.isolated_position_margin_calculation.get(&market_index) + { Ok(isolated_position_margin_calculation) } else { Err(ErrorCode::InvalidMarginCalculation) @@ -648,6 +697,7 @@ impl MarginCalculation { } pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { - self.isolated_position_margin_calculation.contains_key(&market_index) + self.isolated_position_margin_calculation + .contains_key(&market_index) } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 96ecd127bf..6fbef39b6e 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -382,7 +382,6 @@ impl User { self.liquidation_margin_freed = 0; self.last_active_slot = slot; - let liquidation_id = if self.has_isolated_position_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { @@ -410,15 +409,22 @@ impl User { } pub fn has_isolated_position_being_liquidated(&self) -> bool { - self.perp_positions.iter().any(|position| position.is_isolated() && position.is_isolated_position_being_liquidated()) + self.perp_positions.iter().any(|position| { + position.is_isolated() && position.is_isolated_position_being_liquidated() + }) } - pub fn enter_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + pub fn enter_isolated_position_liquidation( + &mut self, + perp_market_index: u16, + ) -> DriftResult { if self.is_isolated_position_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_position_being_liquidated() { + let liquidation_id = if self.is_cross_margin_being_liquidated() + || self.has_isolated_position_being_liquidated() + { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -427,7 +433,7 @@ impl User { let perp_position = self.force_get_isolated_perp_position_mut(perp_market_index)?; perp_position.position_flag |= PositionFlag::BeingLiquidated as u8; - + Ok(liquidation_id) } @@ -437,7 +443,10 @@ impl User { Ok(()) } - pub fn is_isolated_position_being_liquidated(&self, perp_market_index: u16) -> DriftResult { + pub fn is_isolated_position_being_liquidated( + &self, + perp_market_index: u16, + ) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; Ok(perp_position.is_isolated_position_being_liquidated()) } @@ -715,8 +724,7 @@ impl User { isolated_perp_position_market_index: u16, ) -> DriftResult { let strict = margin_requirement_type == MarginRequirementType::Initial; - let context = MarginContext::standard(margin_requirement_type) - .strict(strict); + let context = MarginContext::standard(margin_requirement_type).strict(strict); let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( self, @@ -726,7 +734,8 @@ impl User { context, )?; - let isolated_position_margin_calculation = calculation.get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; + let isolated_position_margin_calculation = calculation + .get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; validate!( calculation.all_liability_oracles_valid, @@ -1255,15 +1264,24 @@ impl PerpPosition { } pub fn is_isolated(&self) -> bool { - self.position_flag & PositionFlag::IsolatedPosition as u8 == PositionFlag::IsolatedPosition as u8 + self.position_flag & PositionFlag::IsolatedPosition as u8 + == PositionFlag::IsolatedPosition as u8 } - pub fn get_isolated_position_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { - get_token_amount(self.isolated_position_scaled_balance as u128, spot_market, &SpotBalanceType::Deposit) + pub fn get_isolated_position_token_amount( + &self, + spot_market: &SpotMarket, + ) -> DriftResult { + get_token_amount( + self.isolated_position_scaled_balance as u128, + spot_market, + &SpotBalanceType::Deposit, + ) } pub fn is_isolated_position_being_liquidated(&self) -> bool { - self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 + self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) + != 0 } } @@ -1281,14 +1299,16 @@ impl SpotBalance for PerpPosition { } fn increase_balance(&mut self, delta: u128) -> DriftResult { - self.isolated_position_scaled_balance = - self.isolated_position_scaled_balance.safe_add(delta.cast::()?)?; + 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::()?)?; + self.isolated_position_scaled_balance = self + .isolated_position_scaled_balance + .safe_sub(delta.cast::()?)?; Ok(()) } diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 09484892ab..64573306a0 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2351,4 +2351,4 @@ mod next_liquidation_id { let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); } -} \ No newline at end of file +} From 2ab06e327893aa8abb30f515a484a82d28e192a8 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 17:33:09 -0400 Subject: [PATCH 034/159] program: add validate for liq borrow for perp pnl --- programs/drift/src/controller/liquidation.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 07d2ee1203..970338b0b6 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -2442,6 +2442,12 @@ pub fn liquidate_borrow_for_perp_pnl( "Perp position must have position pnl" )?; + validate!( + !user_position.is_isolated_position(), + 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)?; From 9a5632650d56837f71207b76852ada3bde81769c Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 18:17:40 -0400 Subject: [PATCH 035/159] program: add test for isolated margin calc --- programs/drift/src/controller/liquidation.rs | 2 +- programs/drift/src/math/margin/tests.rs | 177 +++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 970338b0b6..d02307ce86 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -2443,7 +2443,7 @@ pub fn liquidate_borrow_for_perp_pnl( )?; validate!( - !user_position.is_isolated_position(), + !user_position.is_isolated(), ErrorCode::InvalidPerpPositionToLiquidate, "Perp position is an isolated position" )?; diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 2ad60a5b9e..5a858c3123 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4312,3 +4312,180 @@ mod pools { assert_eq!(result.unwrap_err(), ErrorCode::InvalidPoolId) } } + +#[cfg(test)] +mod isolated_position { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::{create_anchor_account_info, QUOTE_PRECISION_I64}; + 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_account_info}; + + #[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_position_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.cross_margin_meets_margin_requirement(), true); + assert_eq!(isolated_margin_calculation.meets_margin_requirement(), false); + assert_eq!(margin_calculation.isolated_position_meets_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_total_collateral_plus_buffer(); + + let isolated_margin_calculation = margin_calculation.get_isolated_position_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); + } +} \ No newline at end of file From b171c23e3be180be2671e86022026e5cd434cb2d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 15 Aug 2025 18:35:48 -0400 Subject: [PATCH 036/159] is bankrupt test --- programs/drift/src/math/bankruptcy/tests.rs | 43 ++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/math/bankruptcy/tests.rs b/programs/drift/src/math/bankruptcy/tests.rs index 01ba6aad7c..371ab80461 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::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] @@ -81,3 +81,44 @@ fn user_with_empty_position_and_balances() { let is_bankrupt = is_user_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_user_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_user_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_user_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_user_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_user_bankrupt(&user_with_negative_pnl); + assert!(is_bankrupt); +} From 2821269940ea777112e687574120c2d894799d68 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 16:06:03 -0400 Subject: [PATCH 037/159] fix cancel orders --- programs/drift/src/controller/liquidation.rs | 12 ++++++------ programs/drift/src/instructions/user.rs | 2 +- programs/drift/src/state/liquidation_mode.rs | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index d02307ce86..7c4ed9bae8 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -194,7 +194,7 @@ pub fn liquidate_perp( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = + let (cancel_order_market_type, cancel_order_market_index) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, @@ -209,7 +209,7 @@ pub fn liquidate_perp( cancel_order_market_type, cancel_order_market_index, None, - cancel_order_skip_isolated_positions, + true, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -832,7 +832,7 @@ pub fn liquidate_perp_with_fill( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = + let (cancel_orders_market_type, cancel_orders_market_index) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( &mut user, @@ -847,7 +847,7 @@ pub fn liquidate_perp_with_fill( cancel_orders_market_type, cancel_orders_market_index, None, - cancel_orders_is_isolated, + true, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -3007,7 +3007,7 @@ pub fn liquidate_perp_pnl_for_deposit( let liquidation_id = user.enter_cross_margin_liquidation(slot)?; let mut margin_freed = 0_u64; - let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = + let (cancel_orders_market_type, cancel_orders_market_index) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, @@ -3022,7 +3022,7 @@ pub fn liquidate_perp_pnl_for_deposit( cancel_orders_market_type, cancel_orders_market_index, None, - cancel_orders_is_isolated, + true, )?; let (safest_tier_spot_liability, safest_tier_perp_liability) = liquidation_mode diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 82eee452b1..acc7b31f63 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2607,7 +2607,7 @@ pub fn handle_cancel_orders<'c: 'info, 'info>( market_type, market_index, direction, - true, + false, )?; Ok(()) diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 662db067ee..3889c5a1d1 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -37,7 +37,7 @@ pub trait LiquidatePerpMode { fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; - fn get_cancel_orders_params(&self) -> (Option, Option, bool); + fn get_cancel_orders_params(&self) -> (Option, Option); fn calculate_max_pct_to_liquidate( &self, @@ -129,8 +129,8 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.exit_cross_margin_liquidation()) } - fn get_cancel_orders_params(&self) -> (Option, Option, bool) { - (None, None, true) + fn get_cancel_orders_params(&self) -> (Option, Option) { + (None, None) } fn calculate_max_pct_to_liquidate( @@ -280,8 +280,8 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { user.exit_isolated_position_liquidation(self.market_index) } - fn get_cancel_orders_params(&self) -> (Option, Option, bool) { - (Some(MarketType::Perp), Some(self.market_index), true) + fn get_cancel_orders_params(&self) -> (Option, Option) { + (Some(MarketType::Perp), Some(self.market_index)) } fn calculate_max_pct_to_liquidate( From 424987fdc6d6620a4199e630080553f318683359 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 17:16:14 -0400 Subject: [PATCH 038/159] fix set liquidation status --- programs/drift/src/controller/liquidation.rs | 25 ++++---------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 7c4ed9bae8..67237af660 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3740,26 +3740,11 @@ pub fn set_user_status_to_being_liquidated( user.enter_cross_margin_liquidation(slot)?; } - let isolated_position_market_indexes = user - .perp_positions - .iter() - .filter_map(|position| position.is_isolated().then_some(position.market_index)) - .collect::>(); - - // for market_index in isolated_position_market_indexes { - // let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( - // user, - // perp_market_map, - // spot_market_map, - // oracle_map, - // MarginContext::liquidation(liquidation_margin_buffer_ratio).isolated_position_market_index(market_index), - // )?; - - // if !user.is_isolated_position_being_liquidated(market_index)? && !margin_calculation.meets_margin_requirement() { - // user.enter_isolated_position_liquidation(market_index)?; - // } - - // } + for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_position_margin_calculation.iter() { + if !user.is_isolated_position_being_liquidated(*market_index)? && !isolated_position_margin_calculation.meets_margin_requirement() { + user.enter_isolated_position_liquidation(*market_index)?; + } + } Ok(()) } From b84daf15f9c7e692a595fdcc58321c1d13c7b81b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 18:14:39 -0400 Subject: [PATCH 039/159] more tweaks --- programs/drift/src/controller/liquidation.rs | 68 +++++++------------ programs/drift/src/state/liquidation_mode.rs | 12 +++- .../drift/src/state/margin_calculation.rs | 34 +++++----- 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 67237af660..7f8af63d1e 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -107,7 +107,7 @@ pub fn liquidate_perp( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -184,7 +184,7 @@ pub fn liquidate_perp( e })?; - let liquidation_id = user.enter_cross_margin_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)?; @@ -264,7 +264,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -563,12 +563,12 @@ pub fn liquidate_perp( Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - liquidation_mode.increment_free_margin(user, 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 { liquidation_mode.exit_liquidation(user)?; } else if liquidation_mode.should_user_enter_bankruptcy(user)? { - liquidation_mode.enter_bankruptcy(user); + liquidation_mode.enter_bankruptcy(user)?; } let liquidator_meets_initial_margin_requirement = @@ -698,7 +698,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -762,7 +762,7 @@ pub fn liquidate_perp_with_fill( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -822,7 +822,7 @@ pub fn liquidate_perp_with_fill( e })?; - let liquidation_id = user.enter_cross_margin_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)?; @@ -889,7 +889,7 @@ pub fn liquidate_perp_with_fill( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - liquidation_mode.increment_free_margin(&mut user, margin_freed); + liquidation_mode.increment_free_margin(&mut user, margin_freed)?; if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { let (margin_requirement, total_collateral, bit_flags) = @@ -902,7 +902,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1132,7 +1132,7 @@ pub fn liquidate_perp_with_fill( )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - liquidation_mode.increment_free_margin(&mut user, margin_freed_for_perp_position); + liquidation_mode.increment_free_margin(&mut user, margin_freed_for_perp_position)?; if margin_calculation_after.meets_margin_requirement() { liquidation_mode.exit_liquidation(&mut user)?; @@ -1156,7 +1156,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1207,7 +1207,7 @@ pub fn liquidate_spot( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -1791,7 +1791,7 @@ pub fn liquidate_spot_with_swap_begin( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2343,7 +2343,7 @@ pub fn liquidate_borrow_for_perp_pnl( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2827,7 +2827,7 @@ pub fn liquidate_perp_pnl_for_deposit( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -3004,7 +3004,7 @@ pub fn liquidate_perp_pnl_for_deposit( return Ok(()); } - let liquidation_id = user.enter_cross_margin_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) = @@ -3048,7 +3048,7 @@ pub fn liquidate_perp_pnl_for_deposit( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - liquidation_mode.increment_free_margin(user, margin_freed); + liquidation_mode.increment_free_margin(user, margin_freed)?; let exiting_liq_territory = liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)?; @@ -3067,7 +3067,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { @@ -3244,7 +3244,7 @@ pub fn liquidate_perp_pnl_for_deposit( Some(liquidation_mode.as_ref()), )?; margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; - liquidation_mode.increment_free_margin(user, margin_freed_from_liability); + liquidation_mode.increment_free_margin(user, margin_freed_from_liability)?; if pnl_transfer >= pnl_transfer_to_cover_margin_shortage { liquidation_mode.exit_liquidation(user)?; @@ -3276,7 +3276,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { perp_market_index, @@ -3320,17 +3320,11 @@ pub fn resolve_perp_bankruptcy( )?; validate!( - !liquidator.is_cross_margin_being_liquidated(), + !liquidator.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; - validate!( - !liquidator.is_cross_margin_bankrupt(), - ErrorCode::UserBankrupt, - "liquidator bankrupt", - )?; - let market = perp_market_map.get_ref(&market_index)?; validate!( @@ -3544,17 +3538,11 @@ pub fn resolve_spot_bankruptcy( )?; validate!( - !liquidator.is_cross_margin_being_liquidated(), + !liquidator.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; - validate!( - !liquidator.is_cross_margin_bankrupt(), - ErrorCode::UserBankrupt, - "liquidator bankrupt", - )?; - let market = spot_market_map.get_ref(&market_index)?; validate!( @@ -3715,13 +3703,7 @@ pub fn set_user_status_to_being_liquidated( state: &State, ) -> DriftResult { validate!( - !user.is_cross_margin_bankrupt(), - ErrorCode::UserBankrupt, - "user bankrupt", - )?; - - validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "user is already being liquidated", )?; @@ -3740,7 +3722,7 @@ pub fn set_user_status_to_being_liquidated( user.enter_cross_margin_liquidation(slot)?; } - for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_position_margin_calculation.iter() { + for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { if !user.is_isolated_position_being_liquidated(*market_index)? && !isolated_position_margin_calculation.meets_margin_requirement() { user.enter_isolated_position_liquidation(*market_index)?; } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 3889c5a1d1..aa98aa99cc 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -33,6 +33,8 @@ pub trait LiquidatePerpMode { 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<()>; @@ -121,6 +123,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(margin_calculation.cross_margin_meets_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.cross_margin_can_exit_liquidation()?) } @@ -276,6 +282,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { margin_calculation.isolated_position_can_exit_liquidation(self.market_index) } + fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { + user.enter_isolated_position_liquidation(self.market_index) + } + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { user.exit_isolated_position_liquidation(self.market_index) } @@ -320,7 +330,7 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { margin_calculation: &MarginCalculation, ) -> DriftResult<(u128, i128, u8)> { let isolated_position_margin_calculation = margin_calculation - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&self.market_index) .safe_unwrap()?; Ok(( diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index b55e11fed3..9491e52378 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -172,7 +172,7 @@ pub struct MarginCalculation { margin_requirement_plus_buffer: u128, #[cfg(test)] pub margin_requirement_plus_buffer: u128, - pub isolated_position_margin_calculation: BTreeMap, + pub isolated_margin_calculations: BTreeMap, pub num_spot_liabilities: u8, pub num_perp_liabilities: u8, pub all_deposit_oracles_valid: bool, @@ -190,14 +190,14 @@ pub struct MarginCalculation { } #[derive(Clone, Copy, Debug, Default)] -pub struct IsolatedPositionMarginCalculation { +pub struct IsolatedMarginCalculation { pub margin_requirement: u128, pub total_collateral: i128, pub total_collateral_buffer: i128, pub margin_requirement_plus_buffer: u128, } -impl IsolatedPositionMarginCalculation { +impl IsolatedMarginCalculation { pub fn get_total_collateral_plus_buffer(&self) -> i128 { self.total_collateral .saturating_add(self.total_collateral_buffer) @@ -228,7 +228,7 @@ impl MarginCalculation { total_collateral_buffer: 0, margin_requirement: 0, margin_requirement_plus_buffer: 0, - isolated_position_margin_calculation: BTreeMap::new(), + isolated_margin_calculations: BTreeMap::new(), num_spot_liabilities: 0, num_perp_liabilities: 0, all_deposit_oracles_valid: true, @@ -311,14 +311,14 @@ impl MarginCalculation { 0 }; - let isolated_position_margin_calculation = IsolatedPositionMarginCalculation { + let isolated_position_margin_calculation = IsolatedMarginCalculation { margin_requirement, total_collateral, total_collateral_buffer, margin_requirement_plus_buffer, }; - self.isolated_position_margin_calculation + self.isolated_margin_calculations .insert(market_index, isolated_position_margin_calculation); if let Some(market_to_track) = self.market_to_track_margin_requirement() { @@ -416,7 +416,7 @@ impl MarginCalculation { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations { if !isolated_position_margin_calculation.meets_margin_requirement() { return false; @@ -434,7 +434,7 @@ impl MarginCalculation { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { return false; @@ -460,7 +460,7 @@ impl MarginCalculation { market_index: u16, ) -> DriftResult { Ok(self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()? .meets_margin_requirement()) @@ -472,7 +472,7 @@ impl MarginCalculation { market_index: u16, ) -> DriftResult { Ok(self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()? .meets_margin_requirement_with_buffer()) @@ -494,7 +494,7 @@ impl MarginCalculation { } Ok(self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()? .meets_margin_requirement_with_buffer()) @@ -519,7 +519,7 @@ impl MarginCalculation { return Err(ErrorCode::InvalidMarginCalculation); } - self.isolated_position_margin_calculation + self.isolated_margin_calculations .get(&market_index) .safe_unwrap()? .margin_shortage() @@ -538,7 +538,7 @@ impl MarginCalculation { }; let margin_requirement = if market_type == MarketType::Perp { - match self.isolated_position_margin_calculation.get(&market_index) { + match self.isolated_margin_calculations.get(&market_index) { Some(isolated_position_margin_calculation) => { isolated_position_margin_calculation.margin_requirement } @@ -566,7 +566,7 @@ impl MarginCalculation { pub fn get_isolated_position_free_collateral(&self, market_index: u16) -> DriftResult { let isolated_position_margin_calculation = self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()?; isolated_position_margin_calculation @@ -686,9 +686,9 @@ impl MarginCalculation { pub fn get_isolated_position_margin_calculation( &self, market_index: u16, - ) -> DriftResult<&IsolatedPositionMarginCalculation> { + ) -> DriftResult<&IsolatedMarginCalculation> { if let Some(isolated_position_margin_calculation) = - self.isolated_position_margin_calculation.get(&market_index) + self.isolated_margin_calculations.get(&market_index) { Ok(isolated_position_margin_calculation) } else { @@ -697,7 +697,7 @@ impl MarginCalculation { } pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { - self.isolated_position_margin_calculation + self.isolated_margin_calculations .contains_key(&market_index) } } From ea0984261451ecb452b9e5279f30c98a58acd000 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 18:36:39 -0400 Subject: [PATCH 040/159] clean up naming --- programs/drift/src/controller/liquidation.rs | 32 ++++----- .../drift/src/controller/liquidation/tests.rs | 6 +- programs/drift/src/controller/orders.rs | 28 ++++---- programs/drift/src/controller/pnl.rs | 2 +- programs/drift/src/instructions/user.rs | 26 ++++---- programs/drift/src/math/bankruptcy.rs | 4 +- programs/drift/src/math/bankruptcy/tests.rs | 24 +++---- programs/drift/src/math/liquidation.rs | 15 +++-- programs/drift/src/math/margin.rs | 24 +++---- programs/drift/src/math/margin/tests.rs | 12 ++-- programs/drift/src/math/orders.rs | 4 +- programs/drift/src/state/liquidation_mode.rs | 46 ++++++------- .../drift/src/state/margin_calculation.rs | 66 +++++++++---------- programs/drift/src/state/user.rs | 32 ++++----- programs/drift/src/state/user/tests.rs | 8 +-- 15 files changed, 165 insertions(+), 164 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 7f8af63d1e..7ea7d3ceb7 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -21,7 +21,7 @@ use crate::controller::spot_balance::{ }; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; use crate::error::{DriftResult, ErrorCode}; -use crate::math::bankruptcy::is_user_bankrupt; +use crate::math::bankruptcy::is_cross_margin_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, QUOTE_PRECISION, @@ -1416,7 +1416,7 @@ pub fn liquidate_spot( msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() - && margin_calculation.cross_margin_can_exit_liquidation()? + && margin_calculation.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); return Ok(()); @@ -1464,7 +1464,7 @@ pub fn liquidate_spot( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1710,7 +1710,7 @@ pub fn liquidate_spot( if liability_transfer >= liability_transfer_to_cover_margin_shortage { user.exit_cross_margin_liquidation(); - } else if is_user_bankrupt(user) { + } else if is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -1949,7 +1949,7 @@ pub fn liquidate_spot_with_swap_begin( msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() - && margin_calculation.cross_margin_can_exit_liquidation()? + && margin_calculation.can_exit_cross_margin_liquidation()? { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); @@ -2020,7 +2020,7 @@ pub fn liquidate_spot_with_swap_begin( }); // must throw error to stop swap - if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { return Err(ErrorCode::InvalidLiquidation); } @@ -2283,9 +2283,9 @@ pub fn liquidate_spot_with_swap_end( margin_freed = margin_freed.safe_add(margin_freed_from_liability)?; user.increment_margin_freed(margin_freed_from_liability)?; - if margin_calulcation_after.cross_margin_can_exit_liquidation()? { + if margin_calulcation_after.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); - } else if is_user_bankrupt(user) { + } else if is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -2530,7 +2530,7 @@ pub fn liquidate_borrow_for_perp_pnl( msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() - && margin_calculation.cross_margin_can_exit_liquidation()? + && margin_calculation.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); return Ok(()); @@ -2574,7 +2574,7 @@ pub fn liquidate_borrow_for_perp_pnl( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.cross_margin_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; @@ -2753,7 +2753,7 @@ pub fn liquidate_borrow_for_perp_pnl( if liability_transfer >= liability_transfer_to_cover_margin_shortage { user.exit_cross_margin_liquidation(); - } else if is_user_bankrupt(user) { + } else if is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -3527,7 +3527,7 @@ pub fn resolve_spot_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - if !user.is_cross_margin_bankrupt() && is_user_bankrupt(user) { + if !user.is_cross_margin_bankrupt() && is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -3636,7 +3636,7 @@ pub fn resolve_spot_bankruptcy( } // exit bankruptcy - if !is_user_bankrupt(user) { + if !is_cross_margin_bankrupt(user) { user.exit_cross_margin_bankruptcy(); } @@ -3722,9 +3722,9 @@ pub fn set_user_status_to_being_liquidated( user.enter_cross_margin_liquidation(slot)?; } - for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { - if !user.is_isolated_position_being_liquidated(*market_index)? && !isolated_position_margin_calculation.meets_margin_requirement() { - user.enter_isolated_position_liquidation(*market_index)?; + 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() { + user.enter_isolated_margin_liquidation(*market_index)?; } } diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 4548cbbb33..57253feaac 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -9396,7 +9396,7 @@ pub mod liquidate_isolated_perp { .unwrap(); let isolated_margin_calculation = margin_calculation - .get_isolated_position_margin_calculation(0) + .get_isolated_margin_calculation(0) .unwrap(); let total_collateral = isolated_margin_calculation.total_collateral; let margin_requirement_plus_buffer = @@ -9558,7 +9558,7 @@ pub mod liquidate_isolated_perp { assert_eq!(user.perp_positions[0].base_asset_amount, 2000000000); assert_eq!( - user.perp_positions[0].is_isolated_position_being_liquidated(), + user.perp_positions[0].is_being_liquidated(), false ); } @@ -9807,7 +9807,7 @@ pub mod liquidate_isolated_perp { .unwrap(); let market_after = perp_market_map.get_ref(&0).unwrap(); - assert!(!user.is_isolated_position_being_liquidated(0).unwrap()); + assert!(!user.is_isolated_margin_being_liquidated(0).unwrap()); assert_eq!(market_after.amm.total_liquidation_fee, 41787043); } } \ No newline at end of file diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index f30722744e..a295ee40d5 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1952,13 +1952,13 @@ fn fulfill_perp_order( if !taker_margin_calculation.meets_margin_requirement() { let (margin_requirement, total_collateral) = if taker_margin_calculation - .has_isolated_position_margin_calculation(market_index) + .has_isolated_margin_calculation(market_index) { - let isolated_position_margin_calculation = taker_margin_calculation - .get_isolated_position_margin_calculation(market_index)?; + let isolated_margin_calculation = taker_margin_calculation + .get_isolated_margin_calculation(market_index)?; ( - isolated_position_margin_calculation.margin_requirement, - isolated_position_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, ) } else { ( @@ -2028,13 +2028,13 @@ fn fulfill_perp_order( if !maker_margin_calculation.meets_margin_requirement() { let (margin_requirement, total_collateral) = if maker_margin_calculation - .has_isolated_position_margin_calculation(market_index) + .has_isolated_margin_calculation(market_index) { - let isolated_position_margin_calculation = maker_margin_calculation - .get_isolated_position_margin_calculation(market_index)?; + let isolated_margin_calculation = maker_margin_calculation + .get_isolated_margin_calculation(market_index)?; ( - isolated_position_margin_calculation.margin_requirement, - isolated_position_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, ) } else { ( @@ -3227,7 +3227,7 @@ pub fn force_cancel_orders( )?; let cross_margin_meets_initial_margin_requirement = - margin_calc.cross_margin_meets_margin_requirement(); + margin_calc.meets_cross_margin_requirement(); let mut total_fee = 0_u64; @@ -3278,9 +3278,9 @@ pub fn force_cancel_orders( continue; } } else { - let isolated_position_meets_margin_requirement = - margin_calc.isolated_position_meets_margin_requirement(market_index)?; - if isolated_position_meets_margin_requirement { + let meets_isolated_margin_requirement = + margin_calc.meets_isolated_margin_requirement(market_index)?; + if meets_isolated_margin_requirement { continue; } } diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 14e4fbd865..92974b04e1 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -266,7 +266,7 @@ pub fn settle_pnl( if user.perp_positions[position_index].is_isolated() { let perp_position = &mut user.perp_positions[position_index]; if pnl_to_settle_with_user < 0 { - let token_amount = perp_position.get_isolated_position_token_amount(spot_market)?; + let token_amount = perp_position.get_isolated_token_amount(spot_market)?; validate!( token_amount >= pnl_to_settle_with_user.unsigned_abs(), diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index acc7b31f63..b2380d685e 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -33,7 +33,7 @@ use crate::instructions::optional_accounts::{ }; use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; -use crate::math::liquidation::is_isolated_position_being_liquidated; +use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; @@ -2002,9 +2002,9 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( drop(spot_market); - if user.is_isolated_position_being_liquidated(perp_market_index)? { + 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_position_being_liquidated( + let is_being_liquidated = is_isolated_margin_being_liquidated( user, &perp_market_map, &spot_market_map, @@ -2014,7 +2014,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_isolated_position_liquidation(perp_market_index)?; + user.exit_isolated_margin_liquidation(perp_market_index)?; } } @@ -2174,9 +2174,9 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( user.exit_cross_margin_liquidation(); } - if user.is_isolated_position_being_liquidated(perp_market_index)? { + 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_position_being_liquidated( + let is_being_liquidated = is_isolated_margin_being_liquidated( user, &perp_market_map, &spot_market_map, @@ -2186,7 +2186,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_isolated_position_liquidation(perp_market_index)?; + user.exit_isolated_margin_liquidation(perp_market_index)?; } } } else { @@ -2194,7 +2194,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let isolated_perp_position_token_amount = user .force_get_isolated_perp_position_mut(perp_market_index)? - .get_isolated_position_token_amount(&spot_market)?; + .get_isolated_token_amount(&spot_market)?; validate!( amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, @@ -2231,8 +2231,8 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( perp_market_index, )?; - if user.is_isolated_position_being_liquidated(perp_market_index)? { - user.exit_isolated_position_liquidation(perp_market_index)?; + 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() { @@ -2326,7 +2326,7 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( user.force_get_isolated_perp_position_mut(perp_market_index)?; let isolated_position_token_amount = - isolated_perp_position.get_isolated_position_token_amount(spot_market)?; + isolated_perp_position.get_isolated_token_amount(spot_market)?; validate!( amount as u128 <= isolated_position_token_amount, @@ -2352,8 +2352,8 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( perp_market_index, )?; - if user.is_isolated_position_being_liquidated(perp_market_index)? { - user.exit_isolated_position_liquidation(perp_market_index)?; + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; } user.update_last_active_slot(slot); diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index f8963c61c4..b6d3f97453 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -5,7 +5,7 @@ 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; @@ -35,7 +35,7 @@ pub fn is_user_bankrupt(user: &User) -> bool { has_liability } -pub fn is_user_isolated_position_bankrupt(user: &User, market_index: u16) -> DriftResult { +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 { diff --git a/programs/drift/src/math/bankruptcy/tests.rs b/programs/drift/src/math/bankruptcy/tests.rs index 371ab80461..d604c38616 100644 --- a/programs/drift/src/math/bankruptcy/tests.rs +++ b/programs/drift/src/math/bankruptcy/tests.rs @@ -1,4 +1,4 @@ -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, PositionFlag, SpotPosition, User}; use crate::test_utils::{get_positions, get_spot_positions}; @@ -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,14 +71,14 @@ 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); } @@ -95,30 +95,30 @@ fn user_with_isolated_position() { let mut user_with_scaled_balance = user.clone(); user_with_scaled_balance.perp_positions[0].isolated_position_scaled_balance = 1000000000000000000; - let is_bankrupt = is_user_bankrupt(&user_with_scaled_balance); + 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_user_bankrupt(&user_with_base_asset_amount); + 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_user_bankrupt(&user_with_open_order); + 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_user_bankrupt(&user_with_positive_pnl); + 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_user_bankrupt(&user_with_negative_pnl); + 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 9f25648c4e..50f6a5aba2 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -213,7 +213,7 @@ pub fn is_user_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - let is_being_liquidated = !margin_calculation.cross_margin_can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.can_exit_cross_margin_liquidation()?; Ok(is_being_liquidated) } @@ -238,7 +238,7 @@ pub fn validate_user_not_being_liquidated( )?; if user.is_cross_margin_being_liquidated() { - if margin_calculation.cross_margin_can_exit_liquidation()? { + if margin_calculation.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); } else { return Err(ErrorCode::UserIsBeingLiquidated); @@ -248,13 +248,14 @@ pub fn validate_user_not_being_liquidated( .perp_positions .iter() .filter(|position| { - position.is_isolated() && position.is_isolated_position_being_liquidated() + 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.isolated_position_can_exit_liquidation(perp_market_index)? { - user.exit_isolated_position_liquidation(perp_market_index)?; + if margin_calculation.can_exit_isolated_margin_liquidation(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; } else { return Err(ErrorCode::UserIsBeingLiquidated); } @@ -265,7 +266,7 @@ pub fn validate_user_not_being_liquidated( } // todo check if this is corrects -pub fn is_isolated_position_being_liquidated( +pub fn is_isolated_margin_being_liquidated( user: &User, market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, @@ -282,7 +283,7 @@ pub fn is_isolated_position_being_liquidated( )?; let is_being_liquidated = - !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; + !margin_calculation.can_exit_isolated_margin_liquidation(perp_market_index)?; Ok(is_being_liquidated) } diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 16f75868ae..c5781a810a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -311,7 +311,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( token_value = 0; } - calculation.add_total_collateral(token_value)?; + calculation.add_isolated_total_collateral(token_value)?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -329,7 +329,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( token_value, token_value, MarketIdentifier::spot(0), @@ -382,7 +382,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( )?; } - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( spot_position.margin_requirement_for_open_orders()?, 0, MarketIdentifier::spot(spot_market.market_index), @@ -399,7 +399,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( } calculation - .add_total_collateral(worst_case_weighted_token_value.cast::()?)?; + .add_isolated_total_collateral(worst_case_weighted_token_value.cast::()?)?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -422,7 +422,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( worst_case_weighted_token_value.unsigned_abs(), worst_case_token_value.unsigned_abs(), MarketIdentifier::spot(spot_market.market_index), @@ -459,13 +459,13 @@ 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_isolated_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_isolated_margin_requirement( worst_case_orders_value.unsigned_abs(), worst_case_orders_value.unsigned_abs(), MarketIdentifier::spot(0), @@ -559,7 +559,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( &strict_quote_price, )?; - calculation.add_isolated_position_margin_calculation( + calculation.add_isolated_margin_calculation( market.market_index, quote_token_value, weighted_pnl, @@ -570,13 +570,13 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(quote_token_value)?; } else { - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( perp_margin_requirement, worst_case_liability_value, MarketIdentifier::perp(market.market_index), )?; - calculation.add_total_collateral(weighted_pnl)?; + calculation.add_isolated_total_collateral(weighted_pnl)?; } #[cfg(feature = "drift-rs")] @@ -799,7 +799,7 @@ pub fn calculate_max_withdrawable_amount( return token_amount.cast(); } - let free_collateral = calculation.get_cross_margin_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) @@ -945,7 +945,7 @@ pub fn calculate_user_equity( if market_position.is_isolated() { let quote_token_amount = - market_position.get_isolated_position_token_amount("e_spot_market)?; + market_position.get_isolated_token_amount("e_spot_market)?; let token_value = get_token_value( quote_token_amount.cast()?, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 5a858c3123..a540357d48 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -2794,7 +2794,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 ); } @@ -4454,7 +4454,7 @@ mod isolated_position { 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_position_margin_calculation(0).unwrap(); + 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; @@ -4463,9 +4463,9 @@ mod isolated_position { assert_eq!(isolated_margin_requirement, 1000000000); assert_eq!(isolated_total_collateral, -900000000); assert_eq!(margin_calculation.meets_margin_requirement(), false); - assert_eq!(margin_calculation.cross_margin_meets_margin_requirement(), true); + assert_eq!(margin_calculation.meets_cross_margin_requirement(), true); assert_eq!(isolated_margin_calculation.meets_margin_requirement(), false); - assert_eq!(margin_calculation.isolated_position_meets_margin_requirement(0).unwrap(), 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, @@ -4477,9 +4477,9 @@ mod isolated_position { .unwrap(); let cross_margin_margin_requirement = margin_calculation.margin_requirement_plus_buffer; - let cross_total_collateral = margin_calculation.get_total_collateral_plus_buffer(); + let cross_total_collateral = margin_calculation.get_cross_total_collateral_plus_buffer(); - let isolated_margin_calculation = margin_calculation.get_isolated_position_margin_calculation(0).unwrap(); + 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(); diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 85e2928959..88a59175d0 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -861,11 +861,11 @@ pub fn calculate_max_perp_order_size( let is_isolated_position = user.perp_positions[position_index].is_isolated(); let free_collateral_before = if is_isolated_position { margin_calculation - .get_isolated_position_free_collateral(market_index)? + .get_isolated_free_collateral(market_index)? .cast::()? } else { margin_calculation - .get_cross_margin_free_collateral()? + .get_cross_free_collateral()? .cast::()? }; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index aa98aa99cc..938b96bac9 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -7,7 +7,7 @@ use crate::{ }, error::{DriftResult, ErrorCode}, math::{ - bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, + bankruptcy::{is_cross_margin_bankrupt, is_isolated_margin_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap, @@ -93,7 +93,7 @@ pub fn get_perp_liquidation_mode( ) -> DriftResult> { let perp_position = user.get_perp_position(market_index)?; let mode: Box = if perp_position.is_isolated() { - Box::new(IsolatedLiquidatePerpMode::new(market_index)) + Box::new(IsolatedMarginLiquidatePerpMode::new(market_index)) } else { Box::new(CrossMarginLiquidatePerpMode::new(market_index)) }; @@ -120,7 +120,7 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { &self, margin_calculation: &MarginCalculation, ) -> DriftResult { - Ok(margin_calculation.cross_margin_meets_margin_requirement()) + Ok(margin_calculation.meets_cross_margin_requirement()) } fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { @@ -128,7 +128,7 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { - Ok(margin_calculation.cross_margin_can_exit_liquidation()?) + Ok(margin_calculation.can_exit_cross_margin_liquidation()?) } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { @@ -161,11 +161,11 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn is_user_bankrupt(&self, user: &User) -> DriftResult { - Ok(is_user_bankrupt(user)) + Ok(user.is_cross_margin_bankrupt()) } fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { - Ok(is_user_bankrupt(user)) + Ok(is_cross_margin_bankrupt(user)) } fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { @@ -256,38 +256,38 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } } -pub struct IsolatedLiquidatePerpMode { +pub struct IsolatedMarginLiquidatePerpMode { pub market_index: u16, } -impl IsolatedLiquidatePerpMode { +impl IsolatedMarginLiquidatePerpMode { pub fn new(market_index: u16) -> Self { Self { market_index } } } -impl LiquidatePerpMode for IsolatedLiquidatePerpMode { +impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult { - user.is_isolated_position_being_liquidated(self.market_index) + user.is_isolated_margin_being_liquidated(self.market_index) } fn meets_margin_requirements( &self, margin_calculation: &MarginCalculation, ) -> DriftResult { - margin_calculation.isolated_position_meets_margin_requirement(self.market_index) + margin_calculation.meets_isolated_margin_requirement(self.market_index) } fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { - margin_calculation.isolated_position_can_exit_liquidation(self.market_index) + margin_calculation.can_exit_isolated_margin_liquidation(self.market_index) } fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { - user.enter_isolated_position_liquidation(self.market_index) + user.enter_isolated_margin_liquidation(self.market_index) } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { - user.exit_isolated_position_liquidation(self.market_index) + user.exit_isolated_margin_liquidation(self.market_index) } fn get_cancel_orders_params(&self) -> (Option, Option) { @@ -310,32 +310,32 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { } fn is_user_bankrupt(&self, user: &User) -> DriftResult { - user.is_isolated_position_bankrupt(self.market_index) + user.is_isolated_margin_bankrupt(self.market_index) } fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { - is_user_isolated_position_bankrupt(user, self.market_index) + is_isolated_margin_bankrupt(user, self.market_index) } fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - user.enter_isolated_position_bankruptcy(self.market_index) + user.enter_isolated_margin_bankruptcy(self.market_index) } fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - user.exit_isolated_position_bankruptcy(self.market_index) + user.exit_isolated_margin_bankruptcy(self.market_index) } fn get_event_fields( &self, margin_calculation: &MarginCalculation, ) -> DriftResult<(u128, i128, u8)> { - let isolated_position_margin_calculation = margin_calculation + let isolated_margin_calculation = margin_calculation .isolated_margin_calculations .get(&self.market_index) .safe_unwrap()?; Ok(( - isolated_position_margin_calculation.margin_requirement, - isolated_position_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, LiquidationBitFlag::IsolatedPosition as u8, )) } @@ -352,7 +352,7 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { let isolated_perp_position = user.get_isolated_perp_position(self.market_index)?; let token_amount = - isolated_perp_position.get_isolated_position_token_amount(spot_market)?; + isolated_perp_position.get_isolated_token_amount(spot_market)?; validate!( token_amount != 0, @@ -396,6 +396,6 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { } fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { - margin_calculation.isolated_position_margin_shortage(self.market_index) + 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 9491e52378..778682c2f1 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -246,7 +246,7 @@ impl MarginCalculation { } } - pub fn add_total_collateral(&mut self, total_collateral: i128) -> DriftResult { + pub fn add_isolated_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 { @@ -259,7 +259,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_margin_requirement( + pub fn add_isolated_margin_requirement( &mut self, margin_requirement: u128, liability_value: u128, @@ -287,7 +287,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_isolated_position_margin_calculation( + pub fn add_isolated_margin_calculation( &mut self, market_index: u16, deposit_value: i128, @@ -311,7 +311,7 @@ impl MarginCalculation { 0 }; - let isolated_position_margin_calculation = IsolatedMarginCalculation { + let isolated_margin_calculation = IsolatedMarginCalculation { margin_requirement, total_collateral, total_collateral_buffer, @@ -319,7 +319,7 @@ impl MarginCalculation { }; self.isolated_margin_calculations - .insert(market_index, isolated_position_margin_calculation); + .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) { @@ -404,21 +404,21 @@ 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 { - let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement(); + let cross_margin_meets_margin_requirement = self.meets_cross_margin_requirement(); if !cross_margin_meets_margin_requirement { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { - if !isolated_position_margin_calculation.meets_margin_requirement() { + if !isolated_margin_calculation.meets_margin_requirement() { return false; } } @@ -428,15 +428,15 @@ impl MarginCalculation { pub fn meets_margin_requirement_with_buffer(&self) -> bool { let cross_margin_meets_margin_requirement = - self.cross_margin_meets_margin_requirement_with_buffer(); + self.meets_cross_margin_requirement_with_buffer(); if !cross_margin_meets_margin_requirement { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { - if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { + if !isolated_margin_calculation.meets_margin_requirement_with_buffer() { return false; } } @@ -445,17 +445,17 @@ impl MarginCalculation { } #[inline(always)] - pub fn cross_margin_meets_margin_requirement(&self) -> bool { + pub fn meets_cross_margin_requirement(&self) -> bool { self.total_collateral >= self.margin_requirement as i128 } #[inline(always)] - pub fn cross_margin_meets_margin_requirement_with_buffer(&self) -> bool { - self.get_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 + 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 isolated_position_meets_margin_requirement( + pub fn meets_isolated_margin_requirement( &self, market_index: u16, ) -> DriftResult { @@ -467,7 +467,7 @@ impl MarginCalculation { } #[inline(always)] - pub fn isolated_position_meets_margin_requirement_with_buffer( + pub fn meets_isolated_margin_requirement_with_buffer( &self, market_index: u16, ) -> DriftResult { @@ -478,16 +478,16 @@ impl MarginCalculation { .meets_margin_requirement_with_buffer()) } - pub fn cross_margin_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.cross_margin_meets_margin_requirement_with_buffer()) + Ok(self.meets_cross_margin_requirement_with_buffer()) } - pub fn isolated_position_can_exit_liquidation(&self, market_index: u16) -> 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); @@ -509,11 +509,11 @@ 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())? .unsigned_abs()) } - pub fn isolated_position_margin_shortage(&self, market_index: u16) -> DriftResult { + 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); @@ -539,8 +539,8 @@ impl MarginCalculation { let margin_requirement = if market_type == MarketType::Perp { match self.isolated_margin_calculations.get(&market_index) { - Some(isolated_position_margin_calculation) => { - isolated_position_margin_calculation.margin_requirement + Some(isolated_margin_calculation) => { + isolated_margin_calculation.margin_requirement } None => self.margin_requirement, } @@ -557,22 +557,22 @@ impl MarginCalculation { .safe_div(margin_requirement) } - pub fn get_cross_margin_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_position_free_collateral(&self, market_index: u16) -> DriftResult { - let isolated_position_margin_calculation = self + 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_position_margin_calculation + isolated_margin_calculation .total_collateral .safe_sub( - isolated_position_margin_calculation + isolated_margin_calculation .margin_requirement .cast::()?, )? @@ -683,20 +683,20 @@ impl MarginCalculation { Ok(()) } - pub fn get_isolated_position_margin_calculation( + pub fn get_isolated_margin_calculation( &self, market_index: u16, ) -> DriftResult<&IsolatedMarginCalculation> { - if let Some(isolated_position_margin_calculation) = + if let Some(isolated_margin_calculation) = self.isolated_margin_calculations.get(&market_index) { - Ok(isolated_position_margin_calculation) + Ok(isolated_margin_calculation) } else { Err(ErrorCode::InvalidMarginCalculation) } } - pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { + 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/user.rs b/programs/drift/src/state/user.rs index 6fbef39b6e..d7f0a60f4b 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -135,7 +135,7 @@ pub struct User { impl User { pub fn is_being_liquidated(&self) -> bool { - self.is_cross_margin_being_liquidated() || self.has_isolated_position_being_liquidated() + self.is_cross_margin_being_liquidated() || self.has_isolated_margin_being_liquidated() } pub fn is_cross_margin_being_liquidated(&self) -> bool { @@ -382,7 +382,7 @@ impl User { self.liquidation_margin_freed = 0; self.last_active_slot = slot; - let liquidation_id = if self.has_isolated_position_being_liquidated() { + let liquidation_id = if self.has_isolated_margin_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -408,22 +408,22 @@ impl User { self.liquidation_margin_freed = 0; } - pub fn has_isolated_position_being_liquidated(&self) -> bool { + pub fn has_isolated_margin_being_liquidated(&self) -> bool { self.perp_positions.iter().any(|position| { - position.is_isolated() && position.is_isolated_position_being_liquidated() + position.is_isolated() && position.is_being_liquidated() }) } - pub fn enter_isolated_position_liquidation( + pub fn enter_isolated_margin_liquidation( &mut self, perp_market_index: u16, ) -> DriftResult { - if self.is_isolated_position_being_liquidated(perp_market_index)? { + 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_position_being_liquidated() + || self.has_isolated_margin_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { @@ -437,34 +437,34 @@ impl User { Ok(liquidation_id) } - pub fn exit_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + 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); Ok(()) } - pub fn is_isolated_position_being_liquidated( + pub fn is_isolated_margin_being_liquidated( &self, perp_market_index: u16, ) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.is_isolated_position_being_liquidated()) + Ok(perp_position.is_being_liquidated()) } - pub fn enter_isolated_position_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + 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::Bankruptcy as u8; Ok(()) } - pub fn exit_isolated_position_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + 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::Bankruptcy as u8); Ok(()) } - pub fn is_isolated_position_bankrupt(&self, perp_market_index: u16) -> DriftResult { + 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::Bankruptcy as u8) != 0) } @@ -735,7 +735,7 @@ impl User { )?; let isolated_position_margin_calculation = calculation - .get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; + .get_isolated_margin_calculation(isolated_perp_position_market_index)?; validate!( calculation.all_liability_oracles_valid, @@ -1268,7 +1268,7 @@ impl PerpPosition { == PositionFlag::IsolatedPosition as u8 } - pub fn get_isolated_position_token_amount( + pub fn get_isolated_token_amount( &self, spot_market: &SpotMarket, ) -> DriftResult { @@ -1279,7 +1279,7 @@ impl PerpPosition { ) } - pub fn is_isolated_position_being_liquidated(&self) -> bool { + pub fn is_being_liquidated(&self) -> bool { self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 } diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 64573306a0..477e3b17b6 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2335,17 +2335,17 @@ mod next_liquidation_id { let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 1); - let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 1); - user.exit_isolated_position_liquidation(1).unwrap(); + user.exit_isolated_margin_liquidation(1).unwrap(); user.exit_cross_margin_liquidation(); - let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); - let liquidation_id = user.enter_isolated_position_liquidation(2).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(2).unwrap(); assert_eq!(liquidation_id, 2); let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); From cc397f0bc75a7f3727c1802ffbc099ec70a8f90c Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 20 Aug 2025 19:14:09 -0400 Subject: [PATCH 041/159] update last active slot for isolated position liq --- programs/drift/src/controller/liquidation.rs | 20 ++++++++++---------- programs/drift/src/state/liquidation_mode.rs | 2 +- programs/drift/src/state/user.rs | 12 +++++++----- programs/drift/src/state/user/tests.rs | 15 ++++++++++----- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 7ea7d3ceb7..a0842b7a90 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -109,7 +109,7 @@ pub fn liquidate_perp( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; validate!( @@ -255,7 +255,7 @@ pub fn liquidate_perp( if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { let (margin_requirement, total_collateral, bit_flags) = - liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -764,7 +764,7 @@ pub fn liquidate_perp_with_fill( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; let market = perp_market_map.get_ref(&market_index)?; @@ -893,7 +893,7 @@ pub fn liquidate_perp_with_fill( if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { let (margin_requirement, total_collateral, bit_flags) = - liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1209,7 +1209,7 @@ pub fn liquidate_spot( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -1793,7 +1793,7 @@ pub fn liquidate_spot_with_swap_begin( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -2345,7 +2345,7 @@ pub fn liquidate_borrow_for_perp_pnl( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; validate!( @@ -2829,7 +2829,7 @@ pub fn liquidate_perp_pnl_for_deposit( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; validate!( @@ -3058,7 +3058,7 @@ pub fn liquidate_perp_pnl_for_deposit( 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(&intermediate_margin_calculation)?; + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3724,7 +3724,7 @@ pub fn set_user_status_to_being_liquidated( 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() { - user.enter_isolated_margin_liquidation(*market_index)?; + user.enter_isolated_margin_liquidation(*market_index, slot)?; } } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 938b96bac9..15394e96b2 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -283,7 +283,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { } fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { - user.enter_isolated_margin_liquidation(self.market_index) + user.enter_isolated_margin_liquidation(self.market_index, slot) } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d7f0a60f4b..31dea2295c 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -380,11 +380,11 @@ impl User { self.add_user_status(UserStatus::BeingLiquidated); self.liquidation_margin_freed = 0; - self.last_active_slot = slot; 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) }; @@ -417,6 +417,7 @@ impl User { 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); @@ -427,6 +428,7 @@ impl User { { self.next_liquidation_id.safe_sub(1)? } else { + self.last_active_slot = slot; get_then_update_id!(self, next_liquidation_id) }; @@ -734,7 +736,7 @@ impl User { context, )?; - let isolated_position_margin_calculation = calculation + let isolated_margin_calculation = calculation .get_isolated_margin_calculation(isolated_perp_position_market_index)?; validate!( @@ -744,11 +746,11 @@ impl User { )?; validate!( - isolated_position_margin_calculation.meets_margin_requirement(), + isolated_margin_calculation.meets_margin_requirement(), ErrorCode::InsufficientCollateral, "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - isolated_position_margin_calculation.total_collateral, - isolated_position_margin_calculation.margin_requirement + isolated_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement )?; Ok(true) diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 477e3b17b6..d5dce7ffde 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2332,23 +2332,28 @@ mod next_liquidation_id { }; user.perp_positions[1] = isolated_position_2; - let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); + 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).unwrap(); + 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).unwrap(); + 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).unwrap(); + 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(1).unwrap(); + let liquidation_id = user.enter_cross_margin_liquidation(6).unwrap(); assert_eq!(liquidation_id, 2); + assert_eq!(user.last_active_slot, 4); } } From 9833303defc24db7ab9d3edbedec162d62577f9b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 10:18:24 -0400 Subject: [PATCH 042/159] another liquidation review --- programs/drift/src/controller/liquidation.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index a0842b7a90..a91cd263c5 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -342,7 +342,6 @@ pub fn liquidate_perp( .get_price_data("e_spot_market.oracle_id())? .price; - // todo how to handle slot not being on perp position? let liquidator_fee = get_liquidation_fee( market.get_base_liquidator_fee(user.is_high_leverage_mode()), market.get_max_liquidation_fee()?, @@ -1134,7 +1133,7 @@ pub fn liquidate_perp_with_fill( margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; liquidation_mode.increment_free_margin(&mut user, margin_freed_for_perp_position)?; - if margin_calculation_after.meets_margin_requirement() { + 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)?; @@ -1412,7 +1411,7 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_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_cross_margin_being_liquidated() @@ -1945,7 +1944,7 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_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_cross_margin_being_liquidated() @@ -2526,7 +2525,7 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_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_cross_margin_being_liquidated() @@ -3718,7 +3717,7 @@ pub fn set_user_status_to_being_liquidated( )?; // todo handle this - if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { user.enter_cross_margin_liquidation(slot)?; } From 9cb040a667f2c05151e82b5fc78e6fe735da645b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 10:28:58 -0400 Subject: [PATCH 043/159] add test --- programs/drift/src/controller/liquidation.rs | 2 +- programs/drift/src/state/user/tests.rs | 52 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index a91cd263c5..44cec26b34 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3728,4 +3728,4 @@ pub fn set_user_status_to_being_liquidated( } Ok(()) -} +} \ No newline at end of file diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index d5dce7ffde..04d1e0177a 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2357,3 +2357,55 @@ mod next_liquidation_id { 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); + } + } +} \ No newline at end of file From 81774968fec1da6e34d34e2fbf42d7a019b90468 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 10:29:40 -0400 Subject: [PATCH 044/159] cargo fmt -- --- programs/drift/src/controller/liquidation.rs | 26 +++++-- .../drift/src/controller/liquidation/tests.rs | 7 +- programs/drift/src/controller/orders.rs | 58 ++++++++------- programs/drift/src/math/bankruptcy/tests.rs | 3 +- programs/drift/src/math/liquidation.rs | 4 +- programs/drift/src/math/margin.rs | 8 ++- programs/drift/src/math/margin/tests.rs | 72 +++++++++++-------- programs/drift/src/state/liquidation_mode.rs | 3 +- .../drift/src/state/margin_calculation.rs | 15 ++-- programs/drift/src/state/user.rs | 20 ++---- programs/drift/src/state/user/tests.rs | 8 ++- 11 files changed, 118 insertions(+), 106 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 44cec26b34..987bd4a80c 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1411,7 +1411,9 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -1944,7 +1946,9 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -2525,7 +2529,9 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -3717,15 +3723,21 @@ pub fn set_user_status_to_being_liquidated( )?; // todo handle this - if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && !margin_calculation.meets_cross_margin_requirement() + { 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() { + 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() + { user.enter_isolated_margin_liquidation(*market_index, slot)?; } } Ok(()) -} \ No newline at end of file +} diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 57253feaac..cf0789520c 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -9557,10 +9557,7 @@ pub mod liquidate_isolated_perp { .unwrap(); assert_eq!(user.perp_positions[0].base_asset_amount, 2000000000); - assert_eq!( - user.perp_positions[0].is_being_liquidated(), - false - ); + assert_eq!(user.perp_positions[0].is_being_liquidated(), false); } #[test] @@ -9810,4 +9807,4 @@ pub mod liquidate_isolated_perp { assert!(!user.is_isolated_margin_being_liquidated(0).unwrap()); assert_eq!(market_after.amm.total_liquidation_fee, 41787043); } -} \ No newline at end of file +} diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index a295ee40d5..0f3f7ac047 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1951,21 +1951,20 @@ 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, - ) - }; + 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 {})", @@ -2027,21 +2026,20 @@ 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, - ) - }; + 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 {})", diff --git a/programs/drift/src/math/bankruptcy/tests.rs b/programs/drift/src/math/bankruptcy/tests.rs index d604c38616..fbc745caf7 100644 --- a/programs/drift/src/math/bankruptcy/tests.rs +++ b/programs/drift/src/math/bankruptcy/tests.rs @@ -93,7 +93,8 @@ fn user_with_isolated_position() { }; let mut user_with_scaled_balance = user.clone(); - user_with_scaled_balance.perp_positions[0].isolated_position_scaled_balance = 1000000000000000000; + 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); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 50f6a5aba2..09d5b2da20 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -247,9 +247,7 @@ pub fn validate_user_not_being_liquidated( let isolated_positions_being_liquidated = user .perp_positions .iter() - .filter(|position| { - position.is_isolated() && position.is_being_liquidated() - }) + .filter(|position| position.is_isolated() && position.is_being_liquidated()) .map(|position| position.market_index) .collect::>(); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index c5781a810a..ece1767bc4 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -398,8 +398,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_weighted_token_value = 0; } - calculation - .add_isolated_total_collateral(worst_case_weighted_token_value.cast::()?)?; + calculation.add_isolated_total_collateral( + worst_case_weighted_token_value.cast::()?, + )?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -459,7 +460,8 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_orders_value = 0; } - calculation.add_isolated_total_collateral(worst_case_orders_value.cast::()?)?; + calculation + .add_isolated_total_collateral(worst_case_orders_value.cast::()?)?; #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(worst_case_orders_value)?; diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index a540357d48..5a544b52d9 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4320,11 +4320,11 @@ mod isolated_position { use anchor_lang::Owner; use solana_program::pubkey::Pubkey; - use crate::{create_anchor_account_info, QUOTE_PRECISION_I64}; + 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, + 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, @@ -4339,7 +4339,7 @@ mod isolated_position { 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_account_info}; + use crate::{create_anchor_account_info, QUOTE_PRECISION_I64}; #[test] pub fn isolated_position_margin_requirement() { @@ -4442,19 +4442,22 @@ mod isolated_position { ..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 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_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; @@ -4464,28 +4467,41 @@ mod isolated_position { 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); + 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 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(); + 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); } -} \ No newline at end of file +} diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 15394e96b2..a388e9c761 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -351,8 +351,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { 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)?; + let token_amount = isolated_perp_position.get_isolated_token_amount(spot_market)?; validate!( token_amount != 0, diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 778682c2f1..ac689ddd66 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -416,8 +416,7 @@ impl MarginCalculation { return false; } - for (_, isolated_margin_calculation) in &self.isolated_margin_calculations - { + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { if !isolated_margin_calculation.meets_margin_requirement() { return false; } @@ -434,8 +433,7 @@ impl MarginCalculation { return false; } - for (_, isolated_margin_calculation) in &self.isolated_margin_calculations - { + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { if !isolated_margin_calculation.meets_margin_requirement_with_buffer() { return false; } @@ -455,10 +453,7 @@ impl MarginCalculation { } #[inline(always)] - pub fn meets_isolated_margin_requirement( - &self, - market_index: u16, - ) -> DriftResult { + pub fn meets_isolated_margin_requirement(&self, market_index: u16) -> DriftResult { Ok(self .isolated_margin_calculations .get(&market_index) @@ -539,9 +534,7 @@ impl MarginCalculation { 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 - } + Some(isolated_margin_calculation) => isolated_margin_calculation.margin_requirement, None => self.margin_requirement, } } else { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 31dea2295c..6acf51b71f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -409,9 +409,9 @@ impl User { } pub fn has_isolated_margin_being_liquidated(&self) -> bool { - self.perp_positions.iter().any(|position| { - position.is_isolated() && position.is_being_liquidated() - }) + self.perp_positions + .iter() + .any(|position| position.is_isolated() && position.is_being_liquidated()) } pub fn enter_isolated_margin_liquidation( @@ -445,10 +445,7 @@ impl User { Ok(()) } - pub fn is_isolated_margin_being_liquidated( - &self, - perp_market_index: u16, - ) -> DriftResult { + pub fn is_isolated_margin_being_liquidated(&self, perp_market_index: u16) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; Ok(perp_position.is_being_liquidated()) } @@ -736,8 +733,8 @@ impl User { context, )?; - let isolated_margin_calculation = calculation - .get_isolated_margin_calculation(isolated_perp_position_market_index)?; + let isolated_margin_calculation = + calculation.get_isolated_margin_calculation(isolated_perp_position_market_index)?; validate!( calculation.all_liability_oracles_valid, @@ -1270,10 +1267,7 @@ impl PerpPosition { == PositionFlag::IsolatedPosition as u8 } - pub fn get_isolated_token_amount( - &self, - spot_market: &SpotMarket, - ) -> DriftResult { + pub fn get_isolated_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { get_token_amount( self.isolated_position_scaled_balance as u128, spot_market, diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 04d1e0177a..53bac03415 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2373,7 +2373,6 @@ mod force_get_isolated_perp_position_mut { }; 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); @@ -2392,7 +2391,10 @@ mod force_get_isolated_perp_position_mut { { 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); + assert_eq!( + isolated_position_mut.position_flag, + PositionFlag::IsolatedPosition as u8 + ); } let isolated_position = PerpPosition { @@ -2408,4 +2410,4 @@ mod force_get_isolated_perp_position_mut { assert_eq!(isolated_position_mut.is_err(), true); } } -} \ No newline at end of file +} From 9a8ec1a020ec50137bdb5e28b03261b355ef6d8d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 11:55:24 -0400 Subject: [PATCH 045/159] tweak naming --- programs/drift/src/math/margin.rs | 18 +++++++++--------- programs/drift/src/state/margin_calculation.rs | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index ece1767bc4..642f3388f1 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -311,7 +311,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( token_value = 0; } - calculation.add_isolated_total_collateral(token_value)?; + calculation.add_cross_margin_total_collateral(token_value)?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -329,7 +329,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( token_value, token_value, MarketIdentifier::spot(0), @@ -382,7 +382,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( )?; } - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( spot_position.margin_requirement_for_open_orders()?, 0, MarketIdentifier::spot(spot_market.market_index), @@ -398,7 +398,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_weighted_token_value = 0; } - calculation.add_isolated_total_collateral( + calculation.add_cross_margin_total_collateral( worst_case_weighted_token_value.cast::()?, )?; @@ -423,7 +423,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_isolated_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), @@ -461,13 +461,13 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( } calculation - .add_isolated_total_collateral(worst_case_orders_value.cast::()?)?; + .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_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( worst_case_orders_value.unsigned_abs(), worst_case_orders_value.unsigned_abs(), MarketIdentifier::spot(0), @@ -572,13 +572,13 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(quote_token_value)?; } else { - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( perp_margin_requirement, worst_case_liability_value, MarketIdentifier::perp(market.market_index), )?; - calculation.add_isolated_total_collateral(weighted_pnl)?; + calculation.add_cross_margin_total_collateral(weighted_pnl)?; } #[cfg(feature = "drift-rs")] diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index ac689ddd66..2bf4f4abd3 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -246,7 +246,7 @@ impl MarginCalculation { } } - pub fn add_isolated_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 { @@ -259,7 +259,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_isolated_margin_requirement( + pub fn add_cross_margin_margin_requirement( &mut self, margin_requirement: u128, liability_value: u128, From 6ddbaf8f6435c95bcf10fd437aab026bfe46c1bf Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 25 Aug 2025 15:22:22 -0300 Subject: [PATCH 046/159] add test to make sure false liquidaiton wont be triggered --- .../drift/src/controller/liquidation/tests.rs | 222 +++++++++++++++++- programs/drift/src/math/margin.rs | 5 +- 2 files changed, 224 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index cf0789520c..08f360d1f1 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -8914,12 +8914,13 @@ mod liquidate_dust_spot_market { 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; + use crate::controller::liquidation::{liquidate_perp, liquidate_spot}; use crate::controller::position::PositionDirection; use crate::create_anchor_account_info; use crate::error::ErrorCode; @@ -9807,4 +9808,223 @@ pub mod liquidate_isolated_perp { 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 + ); + } } diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 642f3388f1..dd9dfe499a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -460,8 +460,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_orders_value = 0; } - calculation - .add_cross_margin_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)?; From 2db290770fc89e37732e4a74e3c06752150b1b1f Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 25 Aug 2025 16:10:48 -0300 Subject: [PATCH 047/159] test meets withdraw --- programs/drift/src/instructions/user.rs | 14 +- programs/drift/src/state/user.rs | 49 +----- programs/drift/src/state/user/tests.rs | 207 ++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 49 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index b2380d685e..1f3f4070e5 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2223,12 +2223,15 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( drop(spot_market); - user.meets_withdraw_margin_requirement_for_isolated_perp_position( + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - perp_market_index, + 0, + 0, + user_stats, + now, )?; if user.is_isolated_margin_being_liquidated(perp_market_index)? { @@ -2344,12 +2347,15 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( )?; } - user.meets_withdraw_margin_requirement_for_isolated_perp_position( + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - perp_market_index, + 0, + 0, + &mut user_stats, + now, )?; if user.is_isolated_margin_being_liquidated(perp_market_index)? { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 6acf51b71f..d7c766862c 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -643,9 +643,8 @@ impl User { 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( @@ -698,9 +697,8 @@ impl User { 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( @@ -714,45 +712,6 @@ impl User { Ok(true) } - pub fn meets_withdraw_margin_requirement_for_isolated_perp_position( - &mut self, - perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, - oracle_map: &mut OracleMap, - margin_requirement_type: MarginRequirementType, - isolated_perp_position_market_index: u16, - ) -> DriftResult { - let strict = margin_requirement_type == MarginRequirementType::Initial; - let context = MarginContext::standard(margin_requirement_type).strict(strict); - - let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( - self, - perp_market_map, - spot_market_map, - oracle_map, - context, - )?; - - let isolated_margin_calculation = - calculation.get_isolated_margin_calculation(isolated_perp_position_market_index)?; - - validate!( - calculation.all_liability_oracles_valid, - ErrorCode::InvalidOracle, - "User attempting to withdraw with outstanding liabilities when an oracle is invalid" - )?; - - validate!( - isolated_margin_calculation.meets_margin_requirement(), - ErrorCode::InsufficientCollateral, - "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - isolated_margin_calculation.total_collateral, - isolated_margin_calculation.margin_requirement - )?; - - Ok(true) - } - pub fn can_skip_auction_duration(&self, user_stats: &UserStats) -> DriftResult { if user_stats.disable_update_perp_bid_ask_twap { return Ok(false); diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 53bac03415..03a98eb77c 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2411,3 +2411,210 @@ mod force_get_isolated_perp_position_mut { } } } + +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_user_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)); + } +} \ No newline at end of file From 8314bbe5ad0efeef7ed8cee8cbd218cd0659fc8d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 11:18:39 -0300 Subject: [PATCH 048/159] change is bankrupt --- programs/drift/src/state/user.rs | 15 +++++++++++++++ programs/drift/src/validation/user.rs | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d7c766862c..c91d46ccce 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -142,6 +142,10 @@ impl User { 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 } @@ -442,6 +446,7 @@ impl User { 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::Bankruptcy as u8); Ok(()) } @@ -450,6 +455,12 @@ impl User { Ok(perp_position.is_being_liquidated()) } + 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); @@ -1238,6 +1249,10 @@ impl PerpPosition { self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 } + + pub fn is_bankrupt(&self) -> bool { + self.position_flag & PositionFlag::Bankruptcy as u8 == PositionFlag::Bankruptcy as u8 + } } impl SpotBalance for PerpPosition { diff --git a/programs/drift/src/validation/user.rs b/programs/drift/src/validation/user.rs index f19851b35f..3f527fed0f 100644 --- a/programs/drift/src/validation/user.rs +++ b/programs/drift/src/validation/user.rs @@ -17,7 +17,7 @@ pub fn validate_user_deletion( )?; validate!( - !user.is_cross_margin_bankrupt(), + !user.is_bankrupt(), ErrorCode::UserCantBeDeleted, "user bankrupt" )?; @@ -87,7 +87,7 @@ pub fn validate_user_is_idle(user: &User, slot: u64, accelerated: bool) -> Drift )?; validate!( - !user.is_cross_margin_bankrupt(), + !user.is_bankrupt(), ErrorCode::UserNotInactive, "user bankrupt" )?; From 654683cc505e0431192ce69ffbc9bcb036ec4d9b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 11:33:25 -0300 Subject: [PATCH 049/159] more --- programs/drift/src/controller/liquidation.rs | 25 +++++--------------- programs/drift/src/instructions/user.rs | 24 +++++++++---------- programs/drift/src/state/user.rs | 2 +- 3 files changed, 19 insertions(+), 32 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 987bd4a80c..0f1fa8dcc0 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1411,9 +1411,7 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_being_liquidated() - && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -1946,9 +1944,7 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_being_liquidated() - && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -2529,9 +2525,7 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() - && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -3722,19 +3716,12 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - // todo handle this - if !user.is_cross_margin_being_liquidated() - && !margin_calculation.meets_cross_margin_requirement() - { + if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { 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() - { + 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() { user.enter_isolated_margin_liquidation(*market_index, slot)?; } } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 1f3f4070e5..5a4a9cc3f3 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -532,7 +532,7 @@ pub fn handle_deposit<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; @@ -712,7 +712,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let spot_market_is_reduce_only = { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; @@ -882,13 +882,13 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_cross_margin_bankrupt(), + !to_user.is_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_cross_margin_bankrupt(), + !from_user.is_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1104,12 +1104,12 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( let clock = Clock::get()?; validate!( - !to_user.is_cross_margin_bankrupt(), + !to_user.is_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_cross_margin_bankrupt(), + !from_user.is_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1577,13 +1577,13 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_cross_margin_bankrupt(), + !to_user.is_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_cross_margin_bankrupt(), + !from_user.is_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1938,7 +1938,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let perp_market = perp_market_map.get_ref(&perp_market_index)?; @@ -2089,7 +2089,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !user.is_cross_margin_bankrupt(), + !user.is_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt" )?; @@ -2297,7 +2297,7 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; { let perp_market = &perp_market_map.get_ref(&perp_market_index)?; @@ -3796,7 +3796,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( let mut user = load_mut!(&ctx.accounts.user)?; let delegate_is_signer = user.delegate == ctx.accounts.authority.key(); - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; math::liquidation::validate_user_not_being_liquidated( &mut user, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index c91d46ccce..0e9d24abbc 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -485,7 +485,7 @@ impl User { } pub fn update_last_active_slot(&mut self, slot: u64) { - if !self.is_cross_margin_being_liquidated() { + if !self.is_being_liquidated() { self.last_active_slot = slot; } self.idle = false; From 4cab732db19193be344b6210ec8dc20654d01962 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 11:40:46 -0300 Subject: [PATCH 050/159] update uses of exit isolated liquidaiton --- programs/drift/src/instructions/user.rs | 27 ++----------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 5a4a9cc3f3..b4da56aacb 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2175,19 +2175,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( } 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, - &mut oracle_map, - perp_market_index, - state.liquidation_margin_buffer_ratio, - )?; - - if !is_being_liquidated { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } + user.exit_isolated_margin_liquidation(perp_market_index)?; } } else { let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; @@ -2239,18 +2227,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( } 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( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - state.liquidation_margin_buffer_ratio, - )?; - - if !is_being_liquidated { - user.exit_cross_margin_liquidation(); - } + user.exit_cross_margin_liquidation(); } } From 6a6a150f7cd2bc2417c4569751b1d75afcd4cd33 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 15:30:32 -0300 Subject: [PATCH 051/159] moar --- programs/drift/src/controller/liquidation/tests.rs | 8 ++++---- programs/drift/src/controller/orders.rs | 14 +++++++------- programs/drift/src/controller/pnl.rs | 4 ++-- programs/drift/src/instructions/admin.rs | 2 +- programs/drift/src/instructions/user.rs | 10 +++++----- programs/drift/src/math/liquidation.rs | 2 +- programs/drift/src/state/user/tests.rs | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 08f360d1f1..cdc97f60c9 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -17,7 +17,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, }; @@ -904,7 +904,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 +930,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, @@ -8931,7 +8931,7 @@ pub mod liquidate_isolated_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, }; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 0f3f7ac047..fffa26e4f8 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -117,7 +117,7 @@ pub fn place_perp_order( )?; } - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; if params.is_update_high_leverage_mode() { if let Some(config) = high_leverage_mode_config { @@ -1034,7 +1034,7 @@ pub fn fill_perp_order( "Order must be triggered first" )?; - if user.is_cross_margin_bankrupt() { + if user.is_bankrupt() { msg!("user is bankrupt"); return Ok((0, 0)); } @@ -2989,7 +2989,7 @@ pub fn trigger_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let mut perp_market = perp_market_map.get_ref_mut(&market_index)?; let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( @@ -3207,7 +3207,7 @@ pub fn force_cancel_orders( ErrorCode::UserIsBeingLiquidated )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -3420,7 +3420,7 @@ pub fn place_spot_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; if options.try_expire_orders { expire_orders( @@ -3750,7 +3750,7 @@ pub fn fill_spot_order( "Order must be triggered first" )?; - if user.is_cross_margin_bankrupt() { + if user.is_bankrupt() { msg!("User is bankrupt"); return Ok(0); } @@ -5243,7 +5243,7 @@ pub fn trigger_spot_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let spot_market = spot_market_map.get_ref(&market_index)?; let (oracle_price_data, oracle_validity) = oracle_map.get_price_data_and_validity( diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 92974b04e1..0dbc3d67f1 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -56,7 +56,7 @@ pub fn settle_pnl( meets_margin_requirement: Option, mode: SettlePnlMode, ) -> DriftResult { - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let now = clock.unix_timestamp; { let spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; @@ -343,7 +343,7 @@ pub fn settle_expired_position( clock: &Clock, state: &State, ) -> DriftResult { - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; // cannot settle pnl this way on a user who is in liquidation territory if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 00e0e645fd..669a6d95b2 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4612,7 +4612,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( return Err(ErrorCode::InsufficientDeposit.into()); } - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index b4da56aacb..cb53da3dc4 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -34,7 +34,7 @@ use crate::instructions::optional_accounts::{ use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; use crate::math::liquidation::is_isolated_margin_being_liquidated; -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::{ @@ -616,7 +616,7 @@ pub fn handle_deposit<'c: 'info, 'info>( drop(spot_market); 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, @@ -3515,7 +3515,7 @@ pub fn handle_update_user_reduce_only( let mut user = load_mut!(ctx.accounts.user)?; validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::LiquidationsOngoing )?; @@ -3531,7 +3531,7 @@ pub fn handle_update_user_advanced_lp( let mut user = load_mut!(ctx.accounts.user)?; validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::LiquidationsOngoing )?; @@ -3547,7 +3547,7 @@ pub fn handle_update_user_protected_maker_orders( let mut user = load_mut!(ctx.accounts.user)?; validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::LiquidationsOngoing )?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 09d5b2da20..f11709a30d 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -198,7 +198,7 @@ pub fn calculate_asset_transfer_for_liability_transfer( Ok(asset_transfer) } -pub fn is_user_being_liquidated( +pub fn is_cross_margin_being_liquidated( user: &User, market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 03a98eb77c..e01024d853 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2432,7 +2432,7 @@ pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { 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, }; From 7c461876d14822b44bac35b4a883aa33cbff7f75 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 16:23:15 -0300 Subject: [PATCH 052/159] moar --- programs/drift/src/controller/liquidation.rs | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 0f1fa8dcc0..67d93ede70 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -107,9 +107,9 @@ pub fn liquidate_perp( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; validate!( @@ -761,9 +761,9 @@ pub fn liquidate_perp_with_fill( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; let market = perp_market_map.get_ref(&market_index)?; @@ -1206,9 +1206,9 @@ pub fn liquidate_spot( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -1790,9 +1790,9 @@ pub fn liquidate_spot_with_swap_begin( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -2342,9 +2342,9 @@ pub fn liquidate_borrow_for_perp_pnl( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; validate!( @@ -2826,9 +2826,9 @@ pub fn liquidate_perp_pnl_for_deposit( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; validate!( @@ -3319,9 +3319,9 @@ pub fn resolve_perp_bankruptcy( )?; validate!( - !liquidator.is_being_liquidated(), - ErrorCode::UserIsBeingLiquidated, - "liquidator being liquidated", + !liquidator.is_bankrupt(), + ErrorCode::UserBankrupt, + "liquidator bankrupt", )?; let market = perp_market_map.get_ref(&market_index)?; @@ -3537,9 +3537,9 @@ pub fn resolve_spot_bankruptcy( )?; validate!( - !liquidator.is_being_liquidated(), - ErrorCode::UserIsBeingLiquidated, - "liquidator being liquidated", + !liquidator.is_bankrupt(), + ErrorCode::UserBankrupt, + "liquidator bankrupt", )?; let market = spot_market_map.get_ref(&market_index)?; From 51ae2eb5b6943fd6a8e5195328ecbe7c384fe12b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 16:39:28 -0300 Subject: [PATCH 053/159] reduce diff --- programs/drift/src/controller/liquidation.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 67d93ede70..08c95ef478 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3324,6 +3324,12 @@ pub fn resolve_perp_bankruptcy( "liquidator bankrupt", )?; + validate!( + !liquidator.is_being_liquidated(), + ErrorCode::UserIsBeingLiquidated, + "liquidator being liquidated", + )?; + let market = perp_market_map.get_ref(&market_index)?; validate!( @@ -3542,6 +3548,12 @@ pub fn resolve_spot_bankruptcy( "liquidator bankrupt", )?; + validate!( + !liquidator.is_being_liquidated(), + ErrorCode::UserIsBeingLiquidated, + "liquidator being liquidated", + )?; + let market = spot_market_map.get_ref(&market_index)?; validate!( @@ -3701,6 +3713,12 @@ pub fn set_user_status_to_being_liquidated( slot: u64, state: &State, ) -> DriftResult { + validate!( + !user.is_bankrupt(), + ErrorCode::UserBankrupt, + "user bankrupt", + )?; + validate!( !user.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, From bae1b6b28e39de05704641994e94d3050c6c52ce Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 17:27:47 -0300 Subject: [PATCH 054/159] moar --- programs/drift/src/controller/liquidation.rs | 24 ++++++++++++----- programs/drift/src/instructions/user.rs | 23 +++++++--------- programs/drift/src/math/liquidation.rs | 1 - programs/drift/src/state/liquidation_mode.rs | 20 +++++++------- .../drift/src/state/margin_calculation.rs | 2 ++ programs/drift/src/state/user.rs | 7 +++-- programs/drift/src/state/user/tests.rs | 27 ++++++++++++++++--- 7 files changed, 67 insertions(+), 37 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 08c95ef478..7c6854d87d 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1411,7 +1411,9 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -1944,7 +1946,9 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -2525,7 +2529,9 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_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_cross_margin_being_liquidated() @@ -3734,12 +3740,18 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && !margin_calculation.meets_cross_margin_requirement() + { 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() { + 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() + { user.enter_isolated_margin_liquidation(*market_index, slot)?; } } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index cb53da3dc4..e901716634 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -33,8 +33,8 @@ use crate::instructions::optional_accounts::{ }; use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; -use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::liquidation::is_cross_margin_being_liquidated; +use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ @@ -2060,6 +2060,12 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( }; emit!(deposit_record); + 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(()) @@ -3514,10 +3520,7 @@ pub fn handle_update_user_reduce_only( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!( - !user.is_being_liquidated(), - ErrorCode::LiquidationsOngoing - )?; + validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_reduce_only_status(reduce_only)?; Ok(()) @@ -3530,10 +3533,7 @@ pub fn handle_update_user_advanced_lp( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!( - !user.is_being_liquidated(), - ErrorCode::LiquidationsOngoing - )?; + validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_advanced_lp_status(advanced_lp)?; Ok(()) @@ -3546,10 +3546,7 @@ pub fn handle_update_user_protected_maker_orders( ) -> Result<()> { let mut user = load_mut!(ctx.accounts.user)?; - validate!( - !user.is_being_liquidated(), - ErrorCode::LiquidationsOngoing - )?; + validate!(!user.is_being_liquidated(), ErrorCode::LiquidationsOngoing)?; validate!( protected_maker_orders != user.is_protected_maker(), diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index f11709a30d..c1adc5ff2d 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -263,7 +263,6 @@ pub fn validate_user_not_being_liquidated( Ok(()) } -// todo check if this is corrects pub fn is_isolated_margin_being_liquidated( user: &User, market_map: &PerpMarketMap, diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index a388e9c761..d509973a0c 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -12,7 +12,7 @@ use crate::{ margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap, }, - state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, + state::margin_calculation::MarginCalculation, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX, }; @@ -296,11 +296,11 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { fn calculate_max_pct_to_liquidate( &self, - user: &User, - margin_shortage: u128, - slot: u64, - initial_pct_to_liquidate: u128, - liquidation_duration: u128, + _user: &User, + _margin_shortage: u128, + _slot: u64, + _initial_pct_to_liquidate: u128, + _liquidation_duration: u128, ) -> DriftResult { Ok(LIQUIDATION_PCT_PRECISION) } @@ -340,7 +340,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { )) } - fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { + fn validate_spot_position(&self, _user: &User, asset_market_index: u16) -> DriftResult<()> { validate!( asset_market_index == QUOTE_SPOT_MARKET_INDEX, ErrorCode::CouldNotFindSpotPosition, @@ -365,9 +365,9 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { fn calculate_user_safest_position_tiers( &self, - user: &User, + _user: &User, perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, + _spot_market_map: &SpotMarketMap, ) -> DriftResult<(AssetTier, ContractTier)> { let contract_tier = perp_market_map.get_ref(&self.market_index)?.contract_tier; @@ -379,7 +379,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { user: &mut User, token_amount: u128, spot_market: &mut SpotMarket, - cumulative_deposit_delta: Option, + _cumulative_deposit_delta: Option, ) -> DriftResult<()> { let perp_position = user.force_get_isolated_perp_position_mut(self.market_index)?; diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 2bf4f4abd3..8003c6dadd 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -216,6 +216,7 @@ impl IsolatedMarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_total_collateral_plus_buffer())? + .min(0) .unsigned_abs()) } } @@ -505,6 +506,7 @@ impl MarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_cross_total_collateral_plus_buffer())? + .min(0) .unsigned_abs()) } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 0e9d24abbc..d1e75f2623 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1233,8 +1233,7 @@ impl PerpPosition { } pub fn is_isolated(&self) -> bool { - self.position_flag & PositionFlag::IsolatedPosition as u8 - == PositionFlag::IsolatedPosition as u8 + self.position_flag & PositionFlag::IsolatedPosition as u8 > 0 } pub fn get_isolated_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { @@ -1247,11 +1246,11 @@ impl PerpPosition { pub fn is_being_liquidated(&self) -> bool { self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) - != 0 + > 0 } pub fn is_bankrupt(&self) -> bool { - self.position_flag & PositionFlag::Bankruptcy as u8 == PositionFlag::Bankruptcy as u8 + self.position_flag & PositionFlag::Bankruptcy as u8 > 0 } } diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index e01024d853..2b9b8394ff 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2609,12 +2609,33 @@ pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { ..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); + 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); + 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)); } -} \ No newline at end of file +} From d2f08ea12448b5aa87dafc38f165ca2f927ed8fc Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 18:25:58 -0300 Subject: [PATCH 055/159] modularize some for tests --- .../drift/src/controller/isolated_position.rs | 417 ++++++++++++++++++ programs/drift/src/controller/mod.rs | 1 + programs/drift/src/instructions/user.rs | 350 ++------------- 3 files changed, 451 insertions(+), 317 deletions(-) create mode 100644 programs/drift/src/controller/isolated_position.rs diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs new file mode 100644 index 0000000000..6159497e5d --- /dev/null +++ b/programs/drift/src/controller/isolated_position.rs @@ -0,0 +1,417 @@ +use std::cell::RefMut; + +use anchor_lang::prelude::*; +use crate::controller::spot_balance::update_spot_balances; +use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; +use crate::error::ErrorCode; +use crate::math::casting::Cast; +use crate::math::liquidation::is_isolated_margin_being_liquidated; +use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; +use crate::state::events::{ + DepositDirection, DepositExplanation, DepositRecord, +}; +use crate::state::perp_market::MarketStatus; +use crate::state::perp_market_map::PerpMarketMap; +use crate::state::oracle_map::OracleMap; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::spot_market::SpotBalanceType; +use crate::state::state::State; +use crate::state::user::{ + User,UserStats, +}; +use crate::validate; +use crate::controller; +use crate::get_then_update_id; + +pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( + user_key: Pubkey, + user: &mut RefMut, + 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, +) -> Result<()> { + validate!( + amount != 0, + ErrorCode::InsufficientDeposit, + "deposit amount cant be 0", + )?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + let perp_market = perp_market_map.get_ref(&perp_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; + + validate!( + user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + validate!( + !matches!(spot_market.status, MarketStatus::Initialized), + ErrorCode::MarketBeingInitialized, + "Market is being initialized" + )?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_price_data), + now, + )?; + + user.increment_total_deposits( + amount, + oracle_price_data.price, + spot_market.get_precision().cast()?, + )?; + + let total_deposits_after = user.total_deposits; + let total_withdraws_after = user.total_withdraws; + + { + let perp_position = user.force_get_isolated_perp_position_mut(perp_market_index)?; + + update_spot_balances( + amount.cast::()?, + &SpotBalanceType::Deposit, + &mut spot_market, + perp_position, + false, + )?; + } + + validate!( + matches!(spot_market.status, MarketStatus::Active), + ErrorCode::MarketActionPaused, + "spot_market not active", + )?; + + drop(spot_market); + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_isolated_margin_being_liquidated( + user, + perp_market_map, + spot_market_map, + oracle_map, + perp_market_index, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } + + user.update_last_active_slot(slot); + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let oracle_price = oracle_price_data.price; + + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Deposit, + amount, + oracle_price, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after, + total_withdraws_after, + market_index: spot_market_index, + explanation: DepositExplanation::None, + transfer_user: None, + }; + + emit!(deposit_record); + + Ok(()) +} + +pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + user: &mut RefMut, + user_stats: &mut RefMut, + 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, +) -> Result<()> { + validate!( + amount != 0, + ErrorCode::DefaultError, + "transfer amount cant be 0", + )?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + { + let perp_market = &perp_market_map.get_ref(&perp_market_index)?; + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + validate!( + user.pool_id == spot_market.pool_id && user.pool_id == perp_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + now, + )?; + } + + if amount > 0 { + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + + let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; + update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Borrow, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, + false, + )?; + + drop(spot_market); + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + )?; + + validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, oracle_map)?; + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } else { + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + + let isolated_perp_position_token_amount = user + .force_get_isolated_perp_position_mut(perp_market_index)? + .get_isolated_token_amount(&spot_market)?; + + validate!( + amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + spot_market_index + )?; + + let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; + update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Borrow, + &mut spot_market, + user.force_get_isolated_perp_position_mut(perp_market_index)?, + false, + )?; + + drop(spot_market); + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + } + + user.update_last_active_slot(slot); + + Ok(()) +} + +pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( + user_key: Pubkey, + user: &mut RefMut, + user_stats: &mut RefMut, + 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, +) -> Result<()> { + validate!( + amount != 0, + ErrorCode::DefaultError, + "withdraw amount cant be 0", + )?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + { + let perp_market = &perp_market_map.get_ref(&perp_market_index)?; + + validate!( + perp_market.quote_spot_market_index == spot_market_index, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + perp_market.quote_spot_market_index, + spot_market_index + )?; + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle_id())?; + + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + now, + )?; + + user.increment_total_withdraws( + amount, + oracle_price_data.price, + spot_market.get_precision().cast()?, + )?; + + let isolated_perp_position = + user.force_get_isolated_perp_position_mut(perp_market_index)?; + + let isolated_position_token_amount = + isolated_perp_position.get_isolated_token_amount(spot_market)?; + + validate!( + amount as u128 <= isolated_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + spot_market_index + )?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + isolated_perp_position, + true, + )?; + } + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + + user.update_last_active_slot(slot); + + let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; + let oracle_price = oracle_map.get_price_data(&spot_market.oracle_id())?.price; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Withdraw, + oracle_price, + amount, + market_index: spot_market_index, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after: user.total_deposits, + total_withdraws_after: user.total_withdraws, + explanation: DepositExplanation::None, + transfer_user: None, + }; + emit!(deposit_record); + + Ok(()) +} \ No newline at end of file diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index 1565eb1174..0a099f7cde 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/instructions/user.rs b/programs/drift/src/instructions/user.rs index e901716634..0e1308762b 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1934,93 +1934,21 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - if amount == 0 { - return Err(ErrorCode::InsufficientDeposit.into()); - } - - 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), + controller::isolated_position::deposit_into_isolated_perp_position( + user_key, + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, now, - )?; - - user.increment_total_deposits( + state, + spot_market_index, + perp_market_index, 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, - &mut 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 spot_market = spot_market_map.get_ref(&spot_market_index)?; controller::token::receive( &ctx.accounts.token_program, @@ -2035,32 +1963,9 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( None }, )?; - ctx.accounts.spot_market_vault.reload()?; - - let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); - let oracle_price = oracle_price_data.price; - - let deposit_record = DepositRecord { - ts: now, - deposit_record_id, - user_authority: user.authority, - user: user_key, - direction: DepositDirection::Deposit, - amount, - oracle_price, - market_deposit_balance: spot_market.deposit_balance, - market_withdraw_balance: spot_market.borrow_balance, - market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, - market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, - total_deposits_after, - total_withdraws_after, - market_index: spot_market_index, - explanation: DepositExplanation::None, - transfer_user: None, - }; - emit!(deposit_record); ctx.accounts.spot_market_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( &spot_market, ctx.accounts.spot_market_vault.amount, @@ -2112,132 +2017,18 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - { - 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), - clock.unix_timestamp, - )?; - } - - if amount > 0 { - let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; - - let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; - update_spot_balances_and_cumulative_deposits( - amount as u128, - &SpotBalanceType::Borrow, - &mut spot_market, - &mut user.spot_positions[spot_position_index], - false, - None, - )?; - - update_spot_balances( - amount as u128, - &SpotBalanceType::Deposit, - &mut spot_market, - user.force_get_isolated_perp_position_mut(perp_market_index)?, - false, - )?; - - drop(spot_market); - - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( - &perp_market_map, - &spot_market_map, - &mut oracle_map, - MarginRequirementType::Initial, - spot_market_index, - amount as u128, - user_stats, - now, - )?; - - validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; - - if user.is_cross_margin_being_liquidated() { - user.exit_cross_margin_liquidation(); - } - - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } - } else { - let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; - - let isolated_perp_position_token_amount = user - .force_get_isolated_perp_position_mut(perp_market_index)? - .get_isolated_token_amount(&spot_market)?; - - validate!( - amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, - ErrorCode::InsufficientCollateral, - "user has insufficient deposit for market {}", - spot_market_index - )?; - - let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; - update_spot_balances_and_cumulative_deposits( - amount as u128, - &SpotBalanceType::Deposit, - &mut spot_market, - &mut user.spot_positions[spot_position_index], - false, - None, - )?; - - update_spot_balances( - amount as u128, - &SpotBalanceType::Borrow, - &mut spot_market, - user.force_get_isolated_perp_position_mut(perp_market_index)?, - false, - )?; - - drop(spot_market); - - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( - &perp_market_map, - &spot_market_map, - &mut oracle_map, - MarginRequirementType::Initial, - 0, - 0, - user_stats, - now, - )?; - - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } - - if user.is_cross_margin_being_liquidated() { - user.exit_cross_margin_liquidation(); - } - } - - user.update_last_active_slot(slot); + controller::isolated_position::transfer_isolated_perp_position_deposit( + user, + user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + spot_market_index, + perp_market_index, + amount, + )?; let spot_market = spot_market_map.get_ref(&spot_market_index)?; math::spot_withdraw::validate_spot_market_vault_amount( @@ -2280,96 +2071,21 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - 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( + controller::isolated_position::withdraw_from_isolated_perp_position( + user_key, + user, + &mut user_stats, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginRequirementType::Initial, - 0, - 0, - &mut user_stats, + slot, now, + spot_market_index, + perp_market_index, + amount, )?; - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } - - user.update_last_active_slot(slot); - - let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; - let oracle_price = oracle_map.get_price_data(&spot_market.oracle_id())?.price; - - let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); - let deposit_record = DepositRecord { - ts: now, - deposit_record_id, - user_authority: user.authority, - user: user_key, - direction: DepositDirection::Withdraw, - oracle_price, - amount, - market_index: spot_market_index, - market_deposit_balance: spot_market.deposit_balance, - market_withdraw_balance: spot_market.borrow_balance, - market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, - market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, - total_deposits_after: user.total_deposits, - total_withdraws_after: user.total_withdraws, - explanation: DepositExplanation::None, - transfer_user: None, - }; - emit!(deposit_record); + let spot_market = spot_market_map.get_ref(&spot_market_index)?; controller::token::send_from_program_vault( &ctx.accounts.token_program, From ba8866a26bbdd2f912fc5c3e7093d9fc826ec341 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 15:36:54 -0300 Subject: [PATCH 056/159] add tests for the pnl for deposit liquidation --- programs/drift/src/controller/amm.rs | 2 +- .../drift/src/controller/liquidation/tests.rs | 367 ++++++++++++++++++ .../drift/src/state/margin_calculation.rs | 4 +- programs/drift/src/state/user.rs | 14 +- 4 files changed, 377 insertions(+), 10 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 5e4871b8a3..b6ed94b8cd 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -15,7 +15,7 @@ use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ - CONCENTRATION_PRECISION, FEE_ADJUSTMENT_MAX, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, + CONCENTRATION_PRECISION, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, K_BPS_UPDATE_SCALE, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, }; use crate::math::cp_curve::get_update_k_result; diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index cdc97f60c9..81170f25fc 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -10028,3 +10028,370 @@ pub mod liquidate_isolated_perp { ); } } + +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::{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::{PositionFlag, UserStats}; + use crate::state::user::{Order, PerpPosition, SpotPosition, User, UserStatus}; + use crate::test_utils::*; + use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; + use crate::controller::liquidation::resolve_perp_bankruptcy; + + #[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, + 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); + + 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); + } +} diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 8003c6dadd..d4b19ad69f 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -216,7 +216,7 @@ impl IsolatedMarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_total_collateral_plus_buffer())? - .min(0) + .max(0) .unsigned_abs()) } } @@ -506,7 +506,7 @@ impl MarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_cross_total_collateral_plus_buffer())? - .min(0) + .max(0) .unsigned_abs()) } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d1e75f2623..717bbc9bb4 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -446,7 +446,7 @@ impl User { 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::Bankruptcy as u8); + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); Ok(()) } @@ -464,19 +464,19 @@ impl User { 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::Bankruptcy 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::Bankruptcy as u8); + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); Ok(()) } pub fn is_isolated_margin_bankrupt(&self, perp_market_index: u16) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.position_flag & (PositionFlag::Bankruptcy as u8) != 0) + Ok(perp_position.position_flag & (PositionFlag::Bankrupt as u8) != 0) } pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { @@ -1245,12 +1245,12 @@ impl PerpPosition { } pub fn is_being_liquidated(&self) -> bool { - self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) + self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankrupt as u8) > 0 } pub fn is_bankrupt(&self) -> bool { - self.position_flag & PositionFlag::Bankruptcy as u8 > 0 + self.position_flag & PositionFlag::Bankrupt as u8 > 0 } } @@ -1753,7 +1753,7 @@ pub enum OrderBitFlag { pub enum PositionFlag { IsolatedPosition = 0b00000001, BeingLiquidated = 0b00000010, - Bankruptcy = 0b00000100, + Bankrupt = 0b00000100, } #[account(zero_copy(unsafe))] From 9fa04fa33bc820f311da736762d420c26d93e582 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 18:01:41 -0300 Subject: [PATCH 057/159] tests for isolated position transfer --- .../drift/src/controller/isolated_position.rs | 25 +- .../src/controller/isolated_position/tests.rs | 1112 +++++++++++++++++ programs/drift/src/instructions/user.rs | 4 +- programs/drift/src/state/user.rs | 9 +- 4 files changed, 1134 insertions(+), 16 deletions(-) create mode 100644 programs/drift/src/controller/isolated_position/tests.rs diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs index 6159497e5d..6545ac65ae 100644 --- a/programs/drift/src/controller/isolated_position.rs +++ b/programs/drift/src/controller/isolated_position.rs @@ -3,7 +3,7 @@ use std::cell::RefMut; use anchor_lang::prelude::*; use crate::controller::spot_balance::update_spot_balances; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; -use crate::error::ErrorCode; +use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; @@ -23,9 +23,12 @@ use crate::validate; use crate::controller; use crate::get_then_update_id; +#[cfg(test)] +mod tests; + pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( user_key: Pubkey, - user: &mut RefMut, + user: &mut User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -35,7 +38,7 @@ pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( spot_market_index: u16, perp_market_index: u16, amount: u64, -) -> Result<()> { +) -> DriftResult<()> { validate!( amount != 0, ErrorCode::InsufficientDeposit, @@ -154,8 +157,8 @@ pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( } pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( - user: &mut RefMut, - user_stats: &mut RefMut, + user: &mut User, + user_stats: &mut UserStats, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -164,7 +167,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( spot_market_index: u16, perp_market_index: u16, amount: i64, -) -> Result<()> { +) -> DriftResult<()> { validate!( amount != 0, ErrorCode::DefaultError, @@ -260,7 +263,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; update_spot_balances_and_cumulative_deposits( - amount as u128, + amount.abs() as u128, &SpotBalanceType::Deposit, &mut spot_market, &mut user.spot_positions[spot_position_index], @@ -269,7 +272,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( )?; update_spot_balances( - amount as u128, + amount.abs() as u128, &SpotBalanceType::Borrow, &mut spot_market, user.force_get_isolated_perp_position_mut(perp_market_index)?, @@ -305,8 +308,8 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( user_key: Pubkey, - user: &mut RefMut, - user_stats: &mut RefMut, + user: &mut User, + user_stats: &mut UserStats, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -315,7 +318,7 @@ pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( spot_market_index: u16, perp_market_index: u16, amount: u64, -) -> Result<()> { +) -> DriftResult<()> { validate!( amount != 0, ErrorCode::DefaultError, 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..af29a122d9 --- /dev/null +++ b/programs/drift/src/controller/isolated_position/tests.rs @@ -0,0 +1,1112 @@ +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_U64, 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, User + }; + use crate::{create_anchor_account_info, test_utils::*}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[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::{create_anchor_account_info, test_utils::*, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn successful_transfer_to_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut user_stats = UserStats::default(); + + transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_I64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 1000000000); + assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + + assert_eq!(user.spot_positions[0].scaled_balance, 0); + } + + #[test] + pub fn fail_to_transfer_to_existing_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_to_transfer_due_to_insufficient_collateral() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 2* SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + 2* QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } + + #[test] + pub fn successful_transfer_from_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + + assert_eq!(user.spot_positions[0].scaled_balance, SPOT_BALANCE_PRECISION_U64); + } + + #[test] + pub fn fail_transfer_from_non_isolated_perp_position() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_transfer_from_isolated_perp_position_due_to_insufficient_collateral() { + let now = 0_i64; + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut user = User::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + base_asset_amount: 100000, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } +} + +pub mod withdraw_from_isolated_perp_position { + use crate::controller::isolated_position::withdraw_from_isolated_perp_position; + use crate::error::ErrorCode; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, + LIQUIDATION_FEE_PRECISION, + PEG_PRECISION, + QUOTE_PRECISION_U64, 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, User, UserStats + }; + use crate::{create_anchor_account_info, test_utils::*, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[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)); + } + +} \ No newline at end of file diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 0e1308762b..e20c7e0e3c 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1912,7 +1912,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( amount: u64, ) -> Result<()> { let user_key = ctx.accounts.user.key(); - let user = &mut load_mut!(ctx.accounts.user)?; + let mut user = load_mut!(ctx.accounts.user)?; let state = &ctx.accounts.state; let clock = Clock::get()?; @@ -1936,7 +1936,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( controller::isolated_position::deposit_into_isolated_perp_position( user_key, - user, + &mut user, &perp_market_map, &spot_market_map, &mut oracle_map, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 717bbc9bb4..03dc229e2c 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -451,8 +451,11 @@ impl User { } pub fn is_isolated_margin_being_liquidated(&self, perp_market_index: u16) -> DriftResult { - let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.is_being_liquidated()) + 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 { @@ -1087,7 +1090,7 @@ 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 } pub fn is_open_position(&self) -> bool { From a732348ad67c6841aac42230decfdbcd18209e93 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 21:09:39 -0300 Subject: [PATCH 058/159] test for update spot balance --- .../src/controller/spot_balance/tests.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 291e3d6516..725d5b5a87 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}; @@ -1948,3 +1950,65 @@ 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)); +} \ No newline at end of file From 91baee3a8d9a1c745b543f6a7866fcc53323f93e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 22:04:14 -0300 Subject: [PATCH 059/159] test for settle pnl --- programs/drift/src/controller/amm.rs | 6 +- programs/drift/src/controller/amm/tests.rs | 115 +++++--- programs/drift/src/controller/pnl.rs | 12 +- programs/drift/src/controller/pnl/tests.rs | 268 +++++++++++++++++- .../drift/src/controller/position/tests.rs | 3 +- 5 files changed, 358 insertions(+), 46 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index b6ed94b8cd..5f162994ee 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -516,7 +516,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 { @@ -664,11 +664,9 @@ 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)? + -get_max_withdraw_for_market_with_token_amount(spot_market, user_quote_token_amount, false)? .cast::()?; max_withdraw_amount.max(user_unsettled_pnl) diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index a2a33fd6d5..cc78f6b39d 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -286,10 +286,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, ) @@ -299,10 +300,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, ) @@ -320,10 +322,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, ) @@ -339,10 +343,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, ) @@ -377,12 +383,14 @@ fn update_pool_balances_test() { let spot_position = SpotPosition::default(); + 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, 100, now).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 100, now).unwrap(); assert_eq!(to_settle_with_user, 0); + 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, -100, now).unwrap(); + 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); @@ -401,8 +409,9 @@ fn update_pool_balances_test() { assert_eq!(pnl_pool_token_amount, 99); assert_eq!(amm_fee_pool_token_amount, 1); + 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, 100, now).unwrap(); + 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(), @@ -420,7 +429,8 @@ 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, @@ -437,10 +447,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, ) @@ -463,10 +474,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, ) @@ -561,7 +573,8 @@ 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); @@ -576,7 +589,8 @@ 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); @@ -588,12 +602,14 @@ 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); } @@ -672,7 +688,8 @@ 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); @@ -687,7 +704,8 @@ 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); @@ -701,14 +719,16 @@ 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); } @@ -804,7 +824,8 @@ 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, @@ -835,7 +856,8 @@ 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, @@ -860,7 +882,8 @@ 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 @@ -870,7 +893,8 @@ 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 @@ -886,7 +910,8 @@ 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; @@ -899,7 +924,8 @@ 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); @@ -913,7 +939,8 @@ 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); @@ -926,7 +953,8 @@ 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); @@ -975,8 +1003,9 @@ 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; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); assert!( - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now + 3600).is_err() + 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; @@ -993,8 +1022,9 @@ 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); @@ -1072,7 +1102,8 @@ 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); @@ -1163,7 +1194,8 @@ 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 @@ -1509,10 +1541,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, ) @@ -1524,10 +1557,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, ) @@ -1542,10 +1576,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, ) @@ -1560,10 +1595,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, ) @@ -1626,10 +1662,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, ) @@ -1641,10 +1678,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, ) @@ -1659,10 +1697,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/pnl.rs b/programs/drift/src/controller/pnl.rs index 0dbc3d67f1..ba88506df6 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -227,10 +227,18 @@ pub fn settle_pnl( let user_unsettled_pnl: i128 = user.perp_positions[position_index].get_claimable_pnl(oracle_price, max_pnl_pool_excess)?; + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + + let user_quote_token_amount = if is_isolated_position { + user.perp_positions[position_index].get_isolated_token_amount(spot_market)?.cast()? + } else { + user.get_quote_spot_position().get_signed_token_amount(spot_market)? + }; + let pnl_to_settle_with_user = update_pool_balances( perp_market, spot_market, - user.get_quote_spot_position(), + user_quote_token_amount, user_unsettled_pnl, now, )?; @@ -263,7 +271,7 @@ pub fn settle_pnl( ); } - if user.perp_positions[position_index].is_isolated() { + 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)?; diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index 4a35df4e49..dcb3f02f3a 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -22,7 +22,7 @@ use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::{OracleGuardRails, State, ValidityGuardRails}; -use crate::state::user::{PerpPosition, SpotPosition, User}; +use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User}; use crate::test_utils::*; use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; use crate::{create_account_info, SettlePnlMode}; @@ -2113,3 +2113,269 @@ 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 7c41bf2591..01f36ef1e3 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -121,10 +121,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, ) From 101e31168b57cb70ecd6e05c480060f24d5fdd45 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 15:58:32 -0400 Subject: [PATCH 060/159] add perp position max margin --- programs/drift/src/instructions/keeper.rs | 15 ++++++++++++ programs/drift/src/instructions/user.rs | 29 +++++++++++++++++++++++ programs/drift/src/lib.rs | 9 +++++++ programs/drift/src/math/margin.rs | 8 ++++++- programs/drift/src/state/user.rs | 7 +++--- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 101ccfb84e..a94479e547 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,4 +1,5 @@ use std::cell::RefMut; +use std::collections::BTreeMap; use std::convert::TryFrom; use anchor_lang::prelude::*; @@ -2708,6 +2709,16 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( let custom_margin_ratio_before = user.max_margin_ratio; user.max_margin_ratio = 0; + let mut perp_position_max_margin_ratio_map = BTreeMap::new(); + for (index, position) in user.perp_positions.iter_mut().enumerate() { + if position.max_margin_ratio == 0 { + continue; + } + + perp_position_max_margin_ratio_map.insert(index, position.max_margin_ratio); + position.max_margin_ratio = 0; + } + let margin_buffer = MARGIN_PRECISION / 100; // 1% buffer let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, @@ -2720,6 +2731,10 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); user.max_margin_ratio = custom_margin_ratio_before; + // loop through margin ratio map and set max margin ratio + for (index, position) in perp_position_max_margin_ratio_map.iter() { + user.perp_positions[*index].max_margin_ratio = *position; + } if margin_calc.num_perp_liabilities > 0 { for position in user.perp_positions.iter().filter(|p| !p.is_available()) { diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index e20c7e0e3c..9119f4c9bc 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3163,6 +3163,20 @@ pub fn handle_update_user_custom_margin_ratio( Ok(()) } +pub fn handle_update_user_perp_position_custom_margin_ratio( + ctx: Context, + _sub_account_id: u16, + perp_market_index: u16, + margin_ratio: u16, +) -> Result<()> { + let mut user = load_mut!(ctx.accounts.user)?; + + let perp_position = user.force_get_perp_position_mut(perp_market_index)?; + perp_position.max_margin_ratio = margin_ratio; + Ok(()) +} + + pub fn handle_update_user_margin_trading_enabled<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, _sub_account_id: u16, @@ -4736,6 +4750,21 @@ pub struct UpdateUser<'info> { pub authority: Signer<'info>, } +#[derive(Accounts)] +#[instruction( + sub_account_id: u16, +)] +pub struct UpdateUserPerpPositionCustomMarginRatio<'info> { + #[account( + mut, + seeds = [b"user", authority.key.as_ref(), sub_account_id.to_le_bytes().as_ref()], + bump, + constraint = can_sign_for_user(&user, &authority)? + )] + pub user: AccountLoader<'info, User>, + pub authority: Signer<'info>, +} + #[derive(Accounts)] pub struct DeleteUser<'info> { #[account( diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 7edf12a991..c1b77fd047 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -382,6 +382,15 @@ pub mod drift { handle_update_user_custom_margin_ratio(ctx, _sub_account_id, margin_ratio) } + pub fn update_user_perp_position_custom_margin_ratio( + ctx: Context, + _sub_account_id: u16, + perp_market_index: u16, + margin_ratio: u16, + ) -> Result<()> { + handle_update_user_perp_position_custom_margin_ratio(ctx, _sub_account_id, perp_market_index, margin_ratio) + } + pub fn update_user_margin_trading_enabled<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, _sub_account_id: u16, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index dd9dfe499a..9954a7ff5e 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -528,6 +528,12 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0, )?; + let perp_position_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { + market_position.max_margin_ratio as u32 + } else { + 0_u32 + }; + let (perp_margin_requirement, weighted_pnl, worst_case_liability_value, base_asset_value) = calculate_perp_position_value_and_pnl( market_position, @@ -535,7 +541,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_price_data, &strict_quote_price, context.margin_type, - user_custom_margin_ratio, + user_custom_margin_ratio.max(perp_position_custom_margin_ratio), user_high_leverage_mode, )?; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 03dc229e2c..2866dcca6f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1073,10 +1073,9 @@ pub struct PerpPosition { /// Used to settle the users lp position /// precision: QUOTE_PRECISION pub last_quote_asset_amount_per_lp: i64, - /// Settling LP position can lead to a small amount of base asset being left over smaller than step size - /// This records that remainder so it can be settled later on - /// precision: BASE_PRECISION - pub remainder_base_asset_amount: i32, + pub padding: [u8; 2], + // custom max margin ratio for perp market + pub max_margin_ratio: u16, /// The market index for the perp market pub market_index: u16, /// The number of open orders From 0bc613242d91c752fb73df6e25177fe881afd897 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 16:59:30 -0400 Subject: [PATCH 061/159] program: test for custom perp position margin ratio --- programs/drift/src/math/margin/tests.rs | 126 ++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 5a544b52d9..eab128ca39 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -1080,6 +1080,132 @@ mod calculate_margin_requirement_and_total_collateral { assert_eq!(total_collateral, 5000000000); // 100 * $100 * .5 } + #[test] + pub fn user_perp_positions_custom_margin_ratio() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + order_step_size: 10000000, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 100 * BASE_PRECISION_I64, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let MarginCalculation { + margin_requirement, .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + assert_eq!(margin_requirement, 20000000000); + + let user = User { + max_margin_ratio: 4 * MARGIN_PRECISION, // 1x leverage + ..user + }; + + let MarginCalculation { + margin_requirement, .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + // user custom margin ratio should override perp position custom margin ratio + assert_eq!(margin_requirement, 40000000000); + } + #[test] pub fn user_dust_deposit() { let slot = 0_u64; From 608928fe5c3c279801010c53919c6a0fa2dec9b5 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 17:24:48 -0400 Subject: [PATCH 062/159] add test for margin calc for disable hlm --- programs/drift/src/instructions/keeper.rs | 26 +--- programs/drift/src/math/margin.rs | 38 ++++++ programs/drift/src/math/margin/tests.rs | 147 ++++++++++++++++++++++ 3 files changed, 188 insertions(+), 23 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index a94479e547..2c10f97453 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -27,6 +27,7 @@ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; @@ -2706,36 +2707,15 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( user.margin_mode = MarginMode::Default; - let custom_margin_ratio_before = user.max_margin_ratio; - user.max_margin_ratio = 0; - - let mut perp_position_max_margin_ratio_map = BTreeMap::new(); - for (index, position) in user.perp_positions.iter_mut().enumerate() { - if position.max_margin_ratio == 0 { - continue; - } - - perp_position_max_margin_ratio_map.insert(index, position.max_margin_ratio); - position.max_margin_ratio = 0; - } - - let margin_buffer = MARGIN_PRECISION / 100; // 1% buffer - let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( - &user, + let margin_calc = get_margin_calculation_for_disable_high_leverage_mode( + &mut user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial).margin_buffer(margin_buffer), )?; let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); - user.max_margin_ratio = custom_margin_ratio_before; - // loop through margin ratio map and set max margin ratio - for (index, position) in perp_position_max_margin_ratio_map.iter() { - user.perp_positions[*index].max_margin_ratio = *position; - } - if margin_calc.num_perp_liabilities > 0 { for position in user.perp_positions.iter().filter(|p| !p.is_available()) { let perp_market = perp_market_map.get_ref(&position.market_index)?; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 9954a7ff5e..cea5b464cf 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -6,6 +6,7 @@ use crate::math::constants::{ }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; +use crate::MARGIN_PRECISION; use crate::{validate, PRICE_PRECISION_I128}; use crate::{validation, PRICE_PRECISION_I64}; @@ -27,6 +28,7 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::{MarketType, OrderFillSimulation, PerpPosition, User}; use num_integer::Roots; use std::cmp::{max, min, Ordering}; +use std::collections::BTreeMap; use super::spot_balance::get_token_amount; @@ -895,6 +897,42 @@ pub fn validate_spot_margin_trading( Ok(()) } +pub fn get_margin_calculation_for_disable_high_leverage_mode( + user: &mut User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, +) -> DriftResult { + let custom_margin_ratio_before = user.max_margin_ratio; + + + let mut perp_position_max_margin_ratio_map = BTreeMap::new(); + for (index, position) in user.perp_positions.iter_mut().enumerate() { + if position.max_margin_ratio == 0 { + continue; + } + + perp_position_max_margin_ratio_map.insert(index, position.max_margin_ratio); + position.max_margin_ratio = 0; + } + + let margin_buffer = MARGIN_PRECISION / 100; // 1% buffer + let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + perp_market_map, + spot_market_map, + oracle_map, + MarginContext::standard(MarginRequirementType::Initial).margin_buffer(margin_buffer), + )?; + + user.max_margin_ratio = custom_margin_ratio_before; + for (index, position) in perp_position_max_margin_ratio_map.iter() { + user.perp_positions[*index].max_margin_ratio = *position; + } + + Ok(margin_calc) +} + pub fn calculate_user_equity( user: &User, perp_market_map: &PerpMarketMap, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index eab128ca39..566628ad9d 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4631,3 +4631,150 @@ mod isolated_position { assert_eq!(isolated_total_collateral, -1000000000); } } + +#[cfg(test)] +mod get_margin_calculation_for_disable_high_leverage_mode { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::{create_account_info, MARGIN_PRECISION}; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::get_pyth_price; + use crate::create_anchor_account_info; + + #[test] + pub fn get_margin_calculation_for_disable_high_leverage_mode() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + order_step_size: 10000000, + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 20000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[1] = PerpPosition { + market_index: 0, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }; + perp_positions[7] = PerpPosition { + market_index: 1, + max_margin_ratio: 5 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }; + + let mut user = User { + orders: [Order::default(); 32], + perp_positions, + spot_positions, + max_margin_ratio: 2 * MARGIN_PRECISION as u32, // .5x leverage + ..User::default() + }; + + let user_before = user.clone(); + + get_margin_calculation_for_disable_high_leverage_mode( + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + // should not change user + assert_eq!(user, user_before); + } +} \ No newline at end of file From fc6bebc3f3669d7b05d40de980b96df3e4cde838 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 17:41:36 -0400 Subject: [PATCH 063/159] update test name --- programs/drift/src/math/margin/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 566628ad9d..ad8bf242fb 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4658,7 +4658,7 @@ mod get_margin_calculation_for_disable_high_leverage_mode { use crate::create_anchor_account_info; #[test] - pub fn get_margin_calculation_for_disable_high_leverage_mode() { + pub fn check_user_not_changed() { let slot = 0_u64; let mut sol_oracle_price = get_pyth_price(100, 6); From 5f3b7d05cade655d5b866f9f475df5653ce8e3c8 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 18:07:37 -0400 Subject: [PATCH 064/159] make max margin ratio persist --- programs/drift/src/controller/position.rs | 11 ++++++++ programs/drift/src/state/user/tests.rs | 31 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/programs/drift/src/controller/position.rs b/programs/drift/src/controller/position.rs index 30150d3c6e..462d441f05 100644 --- a/programs/drift/src/controller/position.rs +++ b/programs/drift/src/controller/position.rs @@ -49,8 +49,19 @@ pub fn add_new_position( .position(|market_position| market_position.is_available()) .ok_or(ErrorCode::MaxNumberOfPositions)?; + let max_margin_ratio = { + let old_position = &user_positions[new_position_index]; + + if old_position.market_index == market_index { + old_position.max_margin_ratio + } else { + 0_u16 + } + }; + let new_market_position = PerpPosition { market_index, + max_margin_ratio, ..PerpPosition::default() }; diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 2b9b8394ff..748e8443a7 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2639,3 +2639,34 @@ pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); } } + +mod force_get_user_perp_position_mut { + use crate::state::user::{PerpPosition, PositionFlag, User}; + + #[test] + fn test() { + let mut user = User::default(); + + let perp_position = PerpPosition { + market_index: 0, + max_margin_ratio: 1, + ..PerpPosition::default() + }; + user.perp_positions[0] = perp_position; + + // if next available slot is same market index and has max margin ratio, persist it + { + let perp_position_mut = user.force_get_perp_position_mut(0).unwrap(); + assert_eq!(perp_position_mut.max_margin_ratio, 1); + } + + // if next available slot is has max margin but different market index, dont persist it + { + let perp_position_mut = user.force_get_perp_position_mut(2).unwrap(); + assert_eq!(perp_position_mut.max_margin_ratio, 0); + } + + assert_eq!(user.perp_positions[0].market_index, 2); + assert_eq!(user.perp_positions[0].max_margin_ratio, 0); + } +} From 3c56869bc4e86692f2436bc7e5ba955080fef258 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 19:03:31 -0400 Subject: [PATCH 065/159] add liquidation mode test --- .../drift/src/controller/liquidation/tests.rs | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 81170f25fc..4ed30b318a 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -10395,3 +10395,220 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { 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::*; + use crate::test_utils::get_pyth_price; + + #[test] + pub fn tests_meets_margin_requirements() { + 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 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); + } + +} + \ No newline at end of file From bf2839e1c97ddc3b6be586a16c102ca61dbd3626 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 19:22:22 -0400 Subject: [PATCH 066/159] more tests to make sure liqudiations dont bleed over --- .../drift/src/controller/liquidation/tests.rs | 223 +++++++++++++++++- 1 file changed, 221 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 4ed30b318a..20997e4ce5 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; @@ -30,7 +31,7 @@ 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}; @@ -2375,6 +2376,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 cross_margin_doesnt_affect_isolated_margin() { + 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 { @@ -10026,6 +10217,35 @@ pub mod liquidate_isolated_perp { .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); } } @@ -10429,7 +10649,6 @@ mod liquidation_mode { #[test] pub fn tests_meets_margin_requirements() { - let now = 0_i64; let slot = 0_u64; let mut sol_oracle_price = get_pyth_price(100, 6); From eb60940c806f210c9cccab68c0402a665abfd628 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 19:25:44 -0400 Subject: [PATCH 067/159] change test name --- programs/drift/src/controller/liquidation/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 20997e4ce5..bbd15d1b2d 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -2378,7 +2378,7 @@ pub mod liquidate_perp { } #[test] - pub fn cross_margin_doesnt_affect_isolated_margin() { + pub fn unhealthy_cross_margin_doesnt_cause_isolated_position_liquidation() { let now = 0_i64; let slot = 0_u64; From 5cef2f8b3f7f6485294a42ef633914b1cb0580b1 Mon Sep 17 00:00:00 2001 From: moosecat Date: Tue, 16 Sep 2025 16:21:40 -0700 Subject: [PATCH 068/159] Bigz/init lp pool (#1884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * program: init lp pool * cargo fmt -- * add total fee fields * add update_target_weights math * program: use sparse matrix for constituent map and update tests * zero copy accounts, init ix (#1578) * update accounts (#1580) * zero copy + permissionless crank ixs (#1581) * program: support negative target weights for borrow-lend * fix tests to work with zero copy * few comment changes * remove discriminator from impl macro * add get_swap_amount, get_swap_fees, get_weight (#1579) * add get_swap_amount, get_swap_fees, get_weight * update accounts * add back ts * rebase * add constituent swap fees * fix swap fee calc (#1582) * add init amm mapping to lp context (#1583) * init constituent * add initializeLpPool test (#1585) * add initializeLpPool test * add check for constituent target weights * add add datum ix * add init tests and invariant checks * rename data to more useful names * dlp use spl token program (#1588) * add crank ix * update total_weight for validation_flags check * push test so far * overriding perp position works * remove message * fix dup total_weight add * constituent map remaining accounts * compiles * bankrun tests pass * compiles but casting failure in overflow protection test * address comment and change token arguments from u64 to u128 * bankrun tests pass * init constituent token account (#1596) * update aum calc * add update /remove mapping ixs * fix test - init constituent spot market * add crank improvements * passes tests * precision fix crank aum * precision fixes and constituent map check for account owner * add passthrough account logic (#1602) * add passthrough account logic * cant read yet * fix all zc alignment issues * make oracle source a u8 on zc struct * Wphan/dlp-swap-ixs (#1592) * add lp_swap ix * rebase * test helpers * swap works * fix swaps, add more cargo tests for fees n swap amt * remove console.logs * address PR comments * merge upstream * post-merge fixes * store bumps on accounts (#1604) * store bumps on accounts * do pda check in constituent map * address comments * Wphan/add liquidity (#1607) * add add remove liquidity fees calc * add liquidity ix * fix init mint and lppool token account, refactor test fees * add removeLiquidity bankrun test * merge upstream * add LPPool.next_mint_redeem_id * program: lp-pool-to-use-target-base-vector (#1615) * init lp pool target-base matrix * working target-base logic * add todos for add/remove liquidity aum * add renames + fix test * add beta and cost to trade in bps to target datum * add more tests * add fields to LP events, fix tests (#1620) * add fields to LP events, fix tests * revert target weight calc * add constituent.next_swap_id, fix cost_to_trade math * dlp jup swap (#1636) * dlp jup swap * add admin client ixs * almost fixed * test working? * update begin and end swap * tweaks * fix math on how much was swapped * remove unnecessary lp pool args * extra account validation * added token account pda checks in other ixs * stablecoin targets (#1638) * is stablecoin * address comments --------- Co-authored-by: Chris Heaney * cleanup * transfer oracle data ix to constituent (#1643) * transfer oracle data ix to constituent * add lib entrypoint * simplify more * add spot market constraint * big cargo test (#1644) * derivative constituents + better testing + bug fixes (#1657) * all tests technically pass * update tests + prettify * bug fixes and tests pass * fix many bugs and finalize logic * deposit/borrow working and changing positions (#1652) * sdk: allow custom coder * program: dlp add upnl for settles to amm cache (#1659) * program: dlp add-upnl-for-settles-to-amm-cache * finish up lp pool transfer from perp market * add amount_to_transfer using diff * merge * add pnl and fee pool accounting + transfer from dlp to perp market --------- Co-authored-by: Nour Alharithi * remove unused accounts coder * move customCoder into sdk, lint * testing: ix: settle perp to dlp, insufficient balance edge case and improvements (#1688) * finish edge case test * aum check also passes * prettify * added more settle test coverage and squash bugs (#1689) * dlp: add constituentMap (#1699) * Nour/gauntlet fee impl (#1698) * added correlation matrix infra * refactor builds * mint redeem handled for usdc * remove liquidity also should work * all tests pass * bankrun tests pass too * update aum considers amm cache (#1701) * prettify (#1702) * Wphan/merge master dlp (#1703) * feat: init swift user orders on user account creation if needed * fix: wrong pushing of swift user orders ixs * fix: broken swift tests * fix: swift -> signed msg * refactor(sdk): update jupiter's api url * fix(sdk): remove error thrown * indicative qutoes server changes * sdk: release v2.121.0-beta.7 * sdK: update market index 33 oracle rr (#1606) * sdk: add to spot constants market index 34 * revert adminClient.ts change * sdk: update spot market constants oracle index 33 * sdk: release v2.121.0-beta.8 * sdk: high leverage mode updates (#1605) * sdk: high leverage mode updates * add optional param for fee calc * update changelog * sdk: release v2.121.0-beta.9 * getPlaceSignedMsgTakerPerpOrderIxs infer HLM mode from bitflags (#1608) * sdk: release v2.121.0-beta.10 * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs (#1610) * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs * bankrun test * sdk: release v2.121.0-beta.11 * sdk: round tick/step size for getVammL2Generateor (#1612) * sdk: round tick/step size for etVammL2Generateor * use standard functions, include in all fcns * fix const declare, rm whitespace * fix posdir sign * sdk: release v2.121.0-beta.12 * sdk: release v2.121.0-beta.13 * sdk: constants market-index-45-46 (#1618) * sdk: release v2.121.0-beta.14 * robustness check for indicative quotes sender (#1621) * robustness check for indicative quotes sender * delete quote from market index of bad quote * sdk: release v2.121.0-beta.15 * Added launchTs for ZEUS, zBTC * sdk: release v2.121.0-beta.16 * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622) * sdk: release v2.121.0-beta.17 * sdk: fix vamm l2 generator base swapped (#1623) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * sdk: release v2.121.0-beta.18 * sdk: revert vamm l2 gen (#1624) * Revert "sdk: fix vamm l2 generator base swapped (#1623)" This reverts commit 56bc78d70e82cb35a90f12f73162bffb640cb655. * Revert "sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622)" This reverts commit e49cfd554cc44cd8d7770184f02f6ddb0bfc92f1. * Revert "sdk: round tick/step size for getVammL2Generateor (#1612)" This reverts commit f932a4ea2afcae314e406b7c7ee35e55b36043ad. * sdk: release v2.121.0-beta.19 * sdk: show protected-asset have zero-borrow-limit (#1603) * sdk: show protected-asset have zero-borrow-limit * rm unused AssetTier import * sdk: release v2.121.0-beta.20 * sdk: market-constants-index-74 (#1629) * sdk: release v2.121.0-beta.21 * program: use saturating_sub for number_of_users (#1616) * program: use saturating_sub for number_of_users * update CHANGELOG.md * program: allow fixing hlm num users (#1630) * sdk: release v2.121.0-beta.22 * sdk: fix switchboard on demand client to use landed at * sdk: release v2.121.0-beta.23 * sdk: spot-market-poolid-4 constants (#1631) * sdk: release v2.121.0-beta.24 * fix high lev mode liq price (#1632) * sdk: release v2.121.0-beta.25 * replace deprecated solana install scripts (#1634) * sdk: release v2.121.0-beta.26 * refactor(sdk): use ReturnType for Timeout types (#1637) * sdk: release v2.121.0-beta.27 * auction price sdk fix * sdk: release v2.121.0-beta.28 * program: multi piecewise interest rate curve (#1560) * program: multi-piecewise-interest-rate-curve * update tests * widen out borrow limits/healthy util check * add break, use array of array for borrow slope segments * program: fix cargo test * sdk: add segmented IR curve to interest rate calc * clean up unusded var, make interest rate segment logic a const * incorp efficiency feedback points * test: add sol realistic market example * cargo fmt -- * CHANGELOG --------- Co-authored-by: Chris Heaney * sdk: release v2.121.0-beta.29 * program: allow hot admin to update market fuel params (#1640) * v2.121.0 * sdk: release v2.122.0-beta.0 * sdk: fix nullish coalescing * sdk: release v2.122.0-beta.1 * program: add logging for wrong perp market mutability * sdk: check free collateral change in maxTradeSizeUsdcForPerp (#1645) * sdk: check free collateral change in maxTradeSizeUsdcForPerp * update changelog * sdk: release v2.122.0-beta.2 * refactor(sdk): emit newSlot event on initial subscribe call (#1646) * sdk: release v2.122.0-beta.3 * sdk: spot-market-constants-pool-id-2 (#1647) * sdk: release v2.122.0-beta.4 * sdk: add-spot-market-index-52-constants (#1649) * sdk: release v2.122.0-beta.5 * program: add existing position fields to order records (#1614) * program: add quote entry amount to order records * fix cargo fmt and test * more reusable code * more reusable code * add another comment * fix math * account for pos flip * fix typo * missed commit * more fixes * align naming * fix typo * CHANGELOG * program: check limit price after applying buffer in trigger limit ord… (#1648) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * fix tests * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: fix cargo tests * program: check limit price when setting auction for limit order (#1650) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * program: check limit price when setting limit auction params * cargo fmt -- * fix CHANGELOG * tests: updates switchboardTxCus.ts * program: try to fix iteration for max order size (#1651) * Revert "program: try to fix iteration for max order size (#1651)" This reverts commit 3f0eab39ed23fa4a9c41cbab9af793c60b50a239. * disable debug logging in bankrun tests * v2.122.0 * sdk: release v2.123.0-beta.0 * sdk: constants-spot-market-index-53 (#1655) * sdk: release v2.123.0-beta.1 * sdk: idl for new existing position order action records * fix: protocol test prettier fix * make ci lut checks not shit * sdk: release v2.123.0-beta.2 * sdk: fix vamm l2 generator base swapped and add new top of book (#1626) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * use proper quoteAmount with baseSwap for top of book orders * clean up console.log * sdk: getVammL2Generator reduce loc (#1628) * sdk: getVammL2Generator-reduce-loc * add MAJORS_TOP_OF_BOOK_QUOTE_AMOUNTS * add marketindex check topOfBookAmounts * yarn lint/prettier * sdk: release v2.123.0-beta.3 * program: allow all limit orders to go through swift (#1661) * program: allow all limit orders to go through swift * add anchor test * CHANGELOG * sdk: add optional initSwiftAccount on existing account deposits (#1660) * sdk: release v2.123.0-beta.4 * program: add taker_speed_bump_override and amm_spread_adjustment * Revert "program: add taker_speed_bump_override and amm_spread_adjustment" This reverts commit 1e19b7e7a6c5cecebdbfb3a9e224a0d4471ba6d2. * program: tests-fee-adjustment-neg-100 (#1656) * program: tests-fee-adjustment-neg-100 * add HLM field to test * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: simplify user can skip duration (#1668) * program: simplify user can skip duration * update context * CHANGELOG * fix test * fix pmm tests --------- Co-authored-by: Chris Heaney * program: add taker_speed_bump_override and amm_spread_adjustment (#1665) * program: add taker_speed_bump_override and amm_spread_adjustment * add admin client * cargo test * add impl for amm_spread_adjustment * ensure no overflows * CHANGELOG * cargo fmt -- * sdk types * prettify --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: update-amm-spread-and-availability-constraints (#1663) * program: update-amm-spread-and-availability-constraints * fix cargo tests * program: use saturating mul for amm spread adj * nour/indic-quotes-sender-v2 (#1667) * nour/indic-quotes-sender-v2 * prettify * pass margin category into calculateEntriesEffectOnFreeCollateral (#1669) * fix cargo test * tests: fix oracle guardrail test * sdk: update idl * yarn prettify:fix * tests: fix a few more place and make tests * prettify fix * whitespace readme change * sdk: release v2.123.0-beta.5 * v2.123.0 * sdk: release v2.124.0-beta.0 * v2.123.0-1 * sdk: calculateVolSpreadBN-sync (#1671) * sdk: release v2.124.0-beta.1 * sdk: calculate-spread-bn-add-amm-spread-adjustment (#1672) * sdk: calculate-spread-bn-add-amm-spread-adjustment * corect sign * add math max 1 * prettify * sdk: release v2.124.0-beta.2 * sdk: correct calculateVolSpreadBN reversion * sdk: release v2.124.0-beta.3 * sdk: add getTriggerAuctionStartPrice (#1654) * sdk: add getTriggerAuctionStartPrice * updates * precisions * remove startBuffer param --------- Co-authored-by: Chris Heaney * sdk: release v2.124.0-beta.4 * feat: customized cadence account loader (#1666) * feat: customized cadence account loader bby * feat: method to read account cadence on custom cadence account loader * feat: PR feedback on customized loader cleaup code and better naming * fix: lint and prettify * feat: more efficient rpc polling on custom polling intervals * feat: custom cadence acct loader override load * chore: prettify * sdk: release v2.124.0-beta.5 * sdk: sync-user-trade-tier-calcs (#1673) * sdk: sync-user-trade-tier-calcs * prettify --------- Co-authored-by: Nick Caradonna * sdk: release v2.124.0-beta.6 * sdk: add new admin client fn * Revert "sdk: add new admin client fn" This reverts commit c7a4f0b174858048bd379f2f2bb0e63595949921. * sdk: release v2.124.0-beta.7 * refactor(ui): add callback logic, fix polling frequency update * sdk: release v2.124.0-beta.8 * program: less order param sanitization for long tail perps (#1680) * program: allow-auction-start-buffer-on-tail-mkt * fix test * cargo fmt -- * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * Wphan/custom coder (#1682) * sdk: allow custom coder * remove unused accounts coder * linter * move customCoder into sdk, lint * update test helpers * update testhelpers.ts * sdk: release v2.124.0-beta.9 * update sdk exports * sdk: release v2.124.0-beta.10 * sdk: safer-calculate-spread-reserve-math (#1681) * sdk: release v2.124.0-beta.11 * update getMaxLeverageForPerp to use usdc logic (#1678) * sdk: release v2.124.0-beta.12 * program: override for oracle delay (#1679) * programy: override for oracle delay * update impl * switch to i8 * CHANGELOG * program: programmatic rebalance between protocol owned if holdings (#1653) * program: if swap * program: add initial config * add update * more * moar * moar * moar * program: update how swap epoch works * add test * add an invariant * cargo fmt -- * add transfer to rev pool * add mint validation * cargo fmt -- * track in amount between tranfsers * add to ci tests * separate key * program: always transfer max amount to rev pool * CHANGELOG * sdk: release v2.124.0-beta.13 * sdk: improve-aclient-accounts-logic (#1684) * sdk: release v2.124.0-beta.14 * program: improve-amm-spread-validates (#1685) * program: let hot wallet update amm jit intensity * sdk: hot wallet can update amm jit intensity * program: hot wallet can update curve intensity * program: fix build * sdk: update idl * sdk: release v2.124.0-beta.15 * v2.124.0 * sdk: release v2.125.0-beta.0 * program: three-point-std-estimator (#1686) * program: three-point-std-estimator * update tests and add sdk * update changelog * sdk: add-updatePerpMarketOracleSlotDelayOverride (#1691) * sdk: release v2.125.0-beta.1 * program: add-amm-inventory-spread-adjustment-param (#1690) * program: add-amm-inventory-spread-adjustment-param * cargo fmt -- * update sdk * prettier * fix syntax { --------- Co-authored-by: Chris Heaney * program: max-apr-rev-settle-by-spot-market (#1692) * program: max-apr-rev-settle-by-spot-market * update max * default to u128 to avoid casts * changelog * sdk: release v2.125.0-beta.2 * program: better account for imf in calculate_max_perp_order_size (#1693) * program: better account for imf in calculate_max_perp_order_size * CHANGELOG * v2.125.0 * sdk: release v2.126.0-beta.0 * sdk: only count taker fee in calculateEntriesEffectOnFreeCollateral for maintenance (#1694) * sdk: release v2.126.0-beta.1 * Separate getAddInsuranceFundStakeIxs (#1695) * sdk: release v2.126.0-beta.2 * idl: amm-inv-adj-latest-idl (#1697) * sdk: release v2.126.0-beta.3 * sdk: spot-market-index-54 constants (#1696) * sdk: release v2.126.0-beta.4 * sdk: update spot market index 54 pythlazer id * sdk: release v2.126.0-beta.5 * Update spotMarkets.ts * sdk: release v2.126.0-beta.6 * prettify --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: Nour Alharithi Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: lil perp Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * slot staleness checks (#1705) * slot staleness checks * update aum ix to use constituent oracles * Nour/derivative constituent testing (#1708) * slot staleness checks * update aum ix to use constituent oracles * constituent test works when adjusting derivative index * constituent depeg kill switch works * works with multiple derivatives on the same parent * remove incorrect usage of nav * fix adminClient and tests * Nour/fee grid search testing (#1714) * grid search * grid search swap test * Nour/address comments (#1715) * low hanging fruit comments * remove pda checks and store lp pool on zero copy accounts * parameterize depeg threshold * make description in lp pool event * update idl for event change * add swap fee unit tests (#1713) * add swap fee unit tests * remove linear inventory fee component * Nour/settle accounting (#1723) * fixing the main settle test and settle function * all current tests pass * update msg occurrences * dont update lp quote owed unless collateralized * Nour/settle testing (#1725) * refactor settle pnl to modularize and add tests * more cargo tests * prettify * Nour/address more comments (#1726) * use oracle staleness threshold for staleness * add spot market vault invariant * refactor update_aum, add unit tests (#1727) * refactor update_aum, add unit tests * add constituent target base tests * update doc * Nour/parameterize dlp (#1731) * add validates and test for withdraw limit * settlement max * update idl * merge conflicts * fixes * update idl * bug fixes * mostly sdk fixes * bug fixes * bug fix and deploy script * program: new amm oracle (#1738) * zero unused amm fields * cargo fmt * bare bones ix * minimal anchor mm oracle impl * update test file * only do admin validate when not anchor test * updates * generalize native entry * fix weird function name chop off * make it compile for --feature cpi (#1748) Co-authored-by: jordy25519 * more efficeint clock and state bit flags check * vamm uses mm oracle (#1747) * add offset * working tests * refactor to use MM oracle as its own type * remove weird preface * sdk updates * bankrun tests all pass * fix test * changes and fixes * widen confidence if mm oracle too diff * sdk side for confidence adjust * changelog * fix lint * fix cargo tests * address comments * add conf check * remove anchor ix and cache oracle confidence * only state admin can reenable mm oracle kill switch * cargo fmt --------- Co-authored-by: jordy25519 * fix tests (#1764) * Nour/move ixs around (#1766) * move around ixs * remove message * add devnet oracle crank wallet * refactored mm oracle * sdk changes + cargo fmt * fix tests * validate price bands with fill fix * normalize fill within price bands * add sdk warning * updated type * undefined guard so anchor tests pass * accept vec for update amm and view amm * adjust test to work with new price bands * Revert "adjust test to work with new price bands" This reverts commit ee40ac8799fa2f6222ea7d0e9b3e07014346a699. * remove price bands logic * add zero ix for mm oracle for reset * add new drift client ix grouping * v1 safety improvements * isolate funding from MM oracle * add cargo tests for amm availability * change oracle validity log bool to enum * address comment * make validate fill direction agnostic * fix liquidate borrow for perp pnl test * fix tests and address comments * commit constituent map to barrel file * add lp fields to perp market account * rearrange perp market struct for lp fields * bug fix for notional position tracking * view function * fee view functions * max aum + whitelist check and removing get_mint_redeem_fee for now * add wsol support for add liquidity * fix sdk and typing bugs * update lp pool params ix * admin override cache and disable settle functions * devnet swap working * dlp taker discovered bug fixes and sdk changes * refactor last settle ts to last settle slot * Nour/settle pnl fix (#1817) * settle perp to lp pool bug fixes * update bankrun test to not use admin fee pool deposit * fix tests using update spot market balances too * add log msgs for withdraw and fix casting bug * check in for z (#1823) * feat: option for custom oracle ws subscriber * fix: pass custom oracle ws sub option in dc constructor * sdk: add spot-market-index-57 to constants (#1815) * sdk: release v2.134.0-beta.2 * lazer oracle migration (#1813) * lazer oracle migration * spot markets too * sdk: release v2.134.0-beta.3 * sdk: release v2.134.0-beta.4 * program: settle pnl invariants (#1812) * program: settle pnl invariants * add test * fix lint * lints * add msg * CHANGELOG * cargo fmt -- * program: add_update_perp_pnl_pool (#1810) * program: add_update_perp_pnl_pool * test * CHANGELOG * sdk: release v2.134.0-beta.5 * program: update-mark-twap-integer-bias (#1783) * program: update-mark-twap-integer-bias * changelog update * program: update-fee-tier-determine-fix5 (#1800) * program: update-fee-tier-determine-fix5 * update changelog * program: update-mark-twap-crank-use-5min-basis (#1769) * program: update-mark-twap-crank-use-5min-basis * changelog * program: update-min-margin-const-limit (#1802) * program: update-min-margin-const-limit * add CHANGELOG.md * sdk: release v2.134.0-beta.6 * program: rm-burn-lp-shares-invariant (#1816) * program: rm-burn-lp-shares-invariant * update changelog * fix test and cargo fmt * fix anchor tests * yarn prettify:fix * reenable settle_pnl mode test * v2.134.0 * sdk: release v2.135.0-beta.0 * Merge pull request #1820 from drift-labs/chester/fix-zod * sdk: release v2.135.0-beta.1 * mm oracle sdk change (#1806) * mm oracle sdk change * better conditional typing * DLOB bug fix * updated idl * rm getAmmBidAskPrice * sdk: release v2.135.0-beta.2 * sdk: fix isHighLeverageMode * sdk: release v2.135.0-beta.3 * refactor(sdk): add update delegate ix method, ovrride authority for settle multiple pnl (#1822) * check in for z * more logging changes * mm oracle sdk additions (#1824) * strict typing for more MM oracle contact points * add comments to auction.ts * prettify * sdk: release v2.135.0-beta.4 * init constituent bug fix and type change * add in invariant to be within 1 bp of balance before after * add strict typing for getPrice and new auction trigger function (#1826) * add strict typing for getPrice and new auction trigger function * refactor getTriggerAuctionStartAndExecutionPrice * sdk: release v2.135.0-beta.5 * update tests and enforce atomic settles for withdraw * add failing withdraw test * withdraw fix * bring diff in validate back to 1 * make lp pool test fail * better failed test * only check after < before, do to spot precision limits * add balance check to be < 1 cent --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: LukasDeco Co-authored-by: lil perp Co-authored-by: wphan Co-authored-by: Chester Sim * add price for lp validates (#1833) * add missing token account reloads and syncs * add disabled lp pool swaps by default * more extensive aum logging * Wphan/merge-builder-codes (#1842) * add RevenueShare and RevenueShareEscrow accounts an init ixs * fix multiple array zc account, and handling different message types in place_signed_msg_taker_order * decoding error * recording orders in RevenueShareEscrow workin * cancel and fill orders * idl * fix sdk build * fix math * update RevenueShareOrder bitflags, store builder_idx instead of pubkey * merge RevenueShareOrders on add * remove builder accounts from cancel ixs, wip settle impl * dont fail settlpnl if no builder users provided * finish settle, rename RevenueShare->Builder, RevenueShareEscrow->BuilderEscrow * feat: option for custom oracle ws subscriber * fix: pass custom oracle ws sub option in dc constructor * sdk: add spot-market-index-57 to constants (#1815) * sdk: release v2.134.0-beta.2 * lazer oracle migration (#1813) * lazer oracle migration * spot markets too * sdk: release v2.134.0-beta.3 * sdk: release v2.134.0-beta.4 * program: settle pnl invariants (#1812) * program: settle pnl invariants * add test * fix lint * lints * add msg * CHANGELOG * cargo fmt -- * program: add_update_perp_pnl_pool (#1810) * program: add_update_perp_pnl_pool * test * CHANGELOG * sdk: release v2.134.0-beta.5 * program: update-mark-twap-integer-bias (#1783) * program: update-mark-twap-integer-bias * changelog update * program: update-fee-tier-determine-fix5 (#1800) * program: update-fee-tier-determine-fix5 * update changelog * program: update-mark-twap-crank-use-5min-basis (#1769) * program: update-mark-twap-crank-use-5min-basis * changelog * program: update-min-margin-const-limit (#1802) * program: update-min-margin-const-limit * add CHANGELOG.md * sdk: release v2.134.0-beta.6 * program: rm-burn-lp-shares-invariant (#1816) * program: rm-burn-lp-shares-invariant * update changelog * fix test and cargo fmt * fix anchor tests * yarn prettify:fix * reenable settle_pnl mode test * v2.134.0 * sdk: release v2.135.0-beta.0 * add more bankrun tests, clean up * clean up, fix tests * why test fail * add subaccountid to BuilderOrder * reduce diff * add referrals * add test can fill settle user with no builderescrow * add referral builder feature flag and referral migration method * fix cargo tests, try fix bankrun test timing issue * Merge pull request #1820 from drift-labs/chester/fix-zod * sdk: release v2.135.0-beta.1 * mm oracle sdk change (#1806) * mm oracle sdk change * better conditional typing * DLOB bug fix * updated idl * rm getAmmBidAskPrice * sdk: release v2.135.0-beta.2 * sdk: fix isHighLeverageMode * sdk: release v2.135.0-beta.3 * refactor(sdk): add update delegate ix method, ovrride authority for settle multiple pnl (#1822) * mm oracle sdk additions (#1824) * strict typing for more MM oracle contact points * add comments to auction.ts * prettify * sdk: release v2.135.0-beta.4 * add strict typing for getPrice and new auction trigger function (#1826) * add strict typing for getPrice and new auction trigger function * refactor getTriggerAuctionStartAndExecutionPrice * sdk: release v2.135.0-beta.5 * sdk: handle unfillable reduce only orders (#1790) * sdk: handle unfillable reduce only orders * fix dlob tests build errors * fix some test build errors * sdk: release v2.135.0-beta.6 * ref price offset amm math fix (#1828) * ref price offset amm math fix * add latest slot optional var to callers of update amm spread * sdk: release v2.135.0-beta.7 * latest slot as argument to getL2 (#1829) * latest slot as argument to getL2 * add comment * update BN import * sdk: release v2.135.0-beta.8 * add SignedMsgOrderParamsMessageV2 * program: trigger price use 5min mark price (#1830) * program: trigger price use 5min mark price * cargo fmt -- --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * v2.135.0 * sdk: release v2.136.0-beta.0 * zero pad swift messages to make backwards compatible * PR feedback * update tests/placeAndMakeSignedMsgBankrun.ts to handle client side errors * lukas/websocket improvements (#1807) * feat: initial implementation for users and markets WS improvements * lukas/gill websocket sub (#1781) * websockets gill temp * feat: feature parity between gill version ws acct sub and reg one + optional passing into driftClient * fix: post rebase bugs and cleanup * chore: websocket account subscriber export * feat: logging string update on ws acct v2 * rm: useless logging * chore: cleanup ws subscriber v2 docs * chore: specific name on custom ws acct sub param * fix: post rebase again cleanup * fix: prettier fixed * feat: initial implementation for users and markets WS improvements * feat: polling check on websocket acct subscriber v2 + naming * fix: lint * fix: non-hanging WS subscription async loop handling * fix: bugs with program ws subs hanging on asynciter * fix: goofy self imports * feat: initial batch fetching temp * temp: sub second WS subscribe time * fix: ws program account subscriber v2 bugs and optimizations * feat: chunk stuff account requests * feat: more subscribe optimizations ws driftclient sub v2 * chore: cleanup ws sub v2 logs * feat: conditional check on using ws account subscriber + unused * fix: bad import * chore: add export of WebSocketProgramAccountSubscriberV2 * fix: unneeded drift idl export messing up common build * fix: consolidate rpc ws subscriptions for oracles * feat: docs for ws v2 and cleanup * chore: more docs on ws acct susbcriber v2 * feat: PR feedback round 2 * fix: default timeout for ws v2 susbcribers * feat: PR feedback on resubOpts and simplify logic * fix: prettier * sdk: release v2.136.0-beta.1 * refactor(sdk): add decimal override for bignum prettyPrint * sdk: release v2.136.0-beta.2 * sdk: while valid tx sender memory leak fix * sdk: release v2.136.0-beta.3 * refactor account logic for borrows * remove double fee count, update tests to check filled position and quote amounts fda * rename Builder -> RevenueShare * add test check accumulated builder/ref fees * fix settle multiple pnl accounts, test ref rewards in multiple markets * [ FIX ] `posaune0423/fix tx fee payer` (#1837) * sdk: release v2.136.0-beta.4 * sdk: add constant for spot market index 58 (#1840) * sdk: add spot market constant 58 * revert .sh * sdk: release v2.136.0-beta.5 * Revert "[ FIX ] `posaune0423/fix tx fee payer` (#1837)" (#1841) This reverts commit 8cc07e0e179d4335fbb47f8aef5ae022b7143550. * sdk: release v2.136.0-beta.6 * express builder fees in tenth of bps * update referral migration params * PR feedback * add builder code feature gate * fix tests * add referral fields * run all tests * kickoff build * disable extra instructions, fix builder code feature flag selection --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: moosecat Co-authored-by: LukasDeco Co-authored-by: lil perp Co-authored-by: Chester Sim Co-authored-by: asuma * calc aum bug fix for borrows * idl changes and drift client working * Wphan/dlp revert builder codes (#1854) * Revert "Wphan/merge-builder-codes (#1842)" This reverts commit c999f83e000436e34ce4cde17700521d24057208. * fix conflicts * fix incorrect merges * address some perp comments * respond to more comments * pda efficiency changes * Revert "pda efficiency changes" This reverts commit 578b957fe9dc6caa6dd1221e357cfc9ddfee0170. * Revert "respond to more comments" This reverts commit 27600a179f57287aeed860a6ae6c6451eb9a62ba. * better wsol handling * subtract exchange fees from amount settled (#1849) * subtract exchange fees from amount settled * add exchange fee scalar to settling * use percents isntead of scalars * re-introduce breaking bchanges * pda efficiency changes * more pda changes * fix tests * merge in crisp token authority changes * address more comments * amm cache rework (#1863) * Crispheaney/lp whitelist mint (#1866) * lp whitelist mint * test * prettify * Crispheaney/zero copy oracle validity (#1865) * amm cache zero copy validity * remove unnecessary fields from amm cache * update pda --------- Co-authored-by: Nour Alharithi * address renaming comments * Nour/cu profiling (#1870) * add CU profiling test * reduce CUs for target base * cache robustness and limit testing CUs * pass through trade ratio in fee calcs * Crispheaney/withdraw in rm liquidity (#1871) * init * fail transfer_from_program_vault if withdraw too big * test * Nour/expand lp status (#1867) * constituent status and paused operations * add admin functions and tests * add lp status checks * testing expanded to lp pool paushed operations on perp markets * make new wallet for lp taker swaps rather than hot wallet * idl changes and bug fixes * improve CUs for target base * more CU opts * more CU reduction in target crank * lp/init lp-settle-records (#1872) * init lp-settle-records * update pr to emit event * add in last settle ts --------- Co-authored-by: Nour Alharithi * remove more unused lp pool params * update idl * update constituent target params vals * update idl * Crispheaney/rm mint (#1875) * rm unnecessary mints * sdk updates --------- Co-authored-by: Nour Alharithi * add constituent map memcmp and lp status on cache * update tests and amm cache iteration method * change target base ix ordering * update admin client whitelistdlp token ix and other things --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> Co-authored-by: wphan Co-authored-by: Chris Heaney Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> Co-authored-by: asuma --- CHANGELOG.md | 10 - programs/drift/Cargo.toml | 2 +- programs/drift/src/controller/amm.rs | 4 +- programs/drift/src/controller/liquidation.rs | 8 +- programs/drift/src/controller/orders.rs | 4 +- .../drift/src/controller/position/tests.rs | 12 +- programs/drift/src/controller/spot_balance.rs | 2 + programs/drift/src/controller/token.rs | 78 +- programs/drift/src/error.rs | 43 +- programs/drift/src/ids.rs | 8 + programs/drift/src/instructions/admin.rs | 318 +- programs/drift/src/instructions/keeper.rs | 357 +- programs/drift/src/instructions/lp_admin.rs | 1257 +++++ programs/drift/src/instructions/lp_pool.rs | 2007 ++++++++ programs/drift/src/instructions/mod.rs | 4 + programs/drift/src/lib.rs | 322 +- programs/drift/src/math/constants.rs | 2 + programs/drift/src/math/lp_pool.rs | 217 + programs/drift/src/math/mod.rs | 1 + programs/drift/src/math/oracle.rs | 40 +- programs/drift/src/state/amm_cache.rs | 352 ++ programs/drift/src/state/constituent_map.rs | 253 ++ programs/drift/src/state/events.rs | 109 + programs/drift/src/state/lp_pool.rs | 1630 +++++++ programs/drift/src/state/lp_pool/tests.rs | 3436 ++++++++++++++ programs/drift/src/state/mod.rs | 4 + programs/drift/src/state/oracle.rs | 51 + programs/drift/src/state/paused_operations.rs | 52 + programs/drift/src/state/perp_market.rs | 57 +- programs/drift/src/state/perp_market/tests.rs | 2 +- programs/drift/src/state/state.rs | 22 +- programs/drift/src/state/zero_copy.rs | 181 + .../drift/src/validation/sig_verification.rs | 105 +- .../src/validation/sig_verification/tests.rs | 184 + sdk/src/accounts/types.ts | 20 + .../webSocketProgramAccountSubscriberV2.ts | 596 +++ sdk/src/addresses/pda.ts | 114 +- sdk/src/adminClient.ts | 1548 ++++++- sdk/src/constituentMap/constituentMap.ts | 291 ++ .../pollingConstituentAccountSubscriber.ts | 97 + .../webSocketConstituentAccountSubscriber.ts | 112 + sdk/src/driftClient.ts | 1078 ++++- sdk/src/driftClientConfig.ts | 23 +- sdk/src/idl/drift.json | 4035 +++++++++++++++-- sdk/src/index.ts | 5 +- sdk/src/memcmp.ts | 24 +- sdk/src/types.ts | 184 + test-scripts/run-anchor-tests.sh | 4 +- test-scripts/single-anchor-test.sh | 5 +- tests/fixtures/token_2022.so | Bin 0 -> 1382016 bytes tests/lpPool.ts | 1680 +++++++ tests/lpPoolCUs.ts | 663 +++ tests/lpPoolSwap.ts | 1005 ++++ tests/testHelpers.ts | 47 +- 54 files changed, 22066 insertions(+), 599 deletions(-) create mode 100644 programs/drift/src/instructions/lp_admin.rs create mode 100644 programs/drift/src/instructions/lp_pool.rs create mode 100644 programs/drift/src/math/lp_pool.rs create mode 100644 programs/drift/src/state/amm_cache.rs create mode 100644 programs/drift/src/state/constituent_map.rs create mode 100644 programs/drift/src/state/lp_pool.rs create mode 100644 programs/drift/src/state/lp_pool/tests.rs create mode 100644 programs/drift/src/state/zero_copy.rs create mode 100644 programs/drift/src/validation/sig_verification/tests.rs create mode 100644 sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts create mode 100644 sdk/src/constituentMap/constituentMap.ts create mode 100644 sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts create mode 100644 sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts create mode 100755 tests/fixtures/token_2022.so create mode 100644 tests/lpPool.ts create mode 100644 tests/lpPoolCUs.ts create mode 100644 tests/lpPoolSwap.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3492f6be78..83366029a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,16 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking -## [2.135.0] - 2025-08-22 - -### Features - -### Fixes - -- program: trigger price use 5min mark price ([#1830](https://github.com/drift-labs/protocol-v2/pull/1830)) - -### Breaking - ## [2.134.0] - 2025-08-13 ### Features diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 945e055dee..87d0bb85e4 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -20,7 +20,7 @@ drift-rs=[] [dependencies] anchor-lang = "0.29.0" solana-program = "1.16" -anchor-spl = "0.29.0" +anchor-spl = { version = "0.29.0", features = [] } pyth-client = "0.2.2" pyth-lazer-solana-contract = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "d790d1cb4da873a949cf33ff70349b7614b232eb", features = ["no-entrypoint"]} pythnet-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 558d0a9e36..1088b680e7 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -15,8 +15,8 @@ use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ - CONCENTRATION_PRECISION, FEE_ADJUSTMENT_MAX, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, - K_BPS_UPDATE_SCALE, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, + CONCENTRATION_PRECISION, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, K_BPS_UPDATE_SCALE, + MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, }; use crate::math::cp_curve::get_update_k_result; use crate::math::repeg::get_total_fee_lower_bound; diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 92d241dbc7..1fd2f548dc 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -50,10 +50,9 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_value; use crate::state::events::{ - emit_stack, LPAction, LPRecord, LiquidateBorrowForPerpPnlRecord, - LiquidatePerpPnlForDepositRecord, LiquidatePerpRecord, LiquidateSpotRecord, LiquidationRecord, - LiquidationType, OrderAction, OrderActionExplanation, OrderActionRecord, OrderRecord, - PerpBankruptcyRecord, SpotBankruptcyRecord, + LiquidateBorrowForPerpPnlRecord, LiquidatePerpPnlForDepositRecord, LiquidatePerpRecord, + LiquidateSpotRecord, LiquidationRecord, LiquidationType, OrderAction, OrderActionExplanation, + OrderActionRecord, OrderRecord, PerpBankruptcyRecord, SpotBankruptcyRecord, }; use crate::state::fill_mode::FillMode; use crate::state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}; @@ -65,7 +64,6 @@ 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::traits::Size; use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User, UserStats}; use crate::state::user_map::{UserMap, UserStatsMap}; use crate::{get_then_update_id, load_mut, LST_POOL_ID}; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6cff2d1350..d1e6db567a 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -48,9 +48,7 @@ use crate::math::spot_balance::{get_signed_token_amount, get_token_amount}; use crate::math::spot_swap::select_margin_type_for_swap; use crate::math::{amm, fees, margin::*, orders::*}; use crate::print_error; -use crate::state::events::{ - emit_stack, get_order_action_record, LPAction, LPRecord, OrderActionRecord, OrderRecord, -}; +use crate::state::events::{emit_stack, get_order_action_record, OrderActionRecord, OrderRecord}; use crate::state::events::{OrderAction, OrderActionExplanation}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index ab6cf1034c..5646807bae 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -1177,7 +1177,7 @@ fn amm_perp_ref_offset() { max_ref_offset, ) .unwrap(); - assert_eq!(res, (perp_market.amm.max_spread / 2) as i32); + assert_eq!(res, 45000); assert_eq!(perp_market.amm.reference_price_offset, 18000); // not updated vs market account let now = 1741207620 + 1; @@ -1256,7 +1256,7 @@ fn amm_perp_ref_offset() { // Uses the original oracle if the slot is old, ignoring MM oracle perp_market.amm.mm_oracle_price = mm_oracle_price_data.get_price() * 995 / 1000; perp_market.amm.mm_oracle_slot = clock_slot - 100; - let mut mm_oracle_price = perp_market + let mm_oracle_price = perp_market .get_mm_oracle_price_data( oracle_price_data, clock_slot, @@ -1264,13 +1264,7 @@ fn amm_perp_ref_offset() { ) .unwrap(); - let _ = _update_amm( - &mut perp_market, - &mut mm_oracle_price, - &state, - now, - clock_slot, - ); + let _ = _update_amm(&mut perp_market, &mm_oracle_price, &state, now, clock_slot); let reserve_price_mm_offset_3 = perp_market.amm.reserve_price().unwrap(); let (b3, a3) = perp_market .amm diff --git a/programs/drift/src/controller/spot_balance.rs b/programs/drift/src/controller/spot_balance.rs index 6e34edb369..100442566e 100644 --- a/programs/drift/src/controller/spot_balance.rs +++ b/programs/drift/src/controller/spot_balance.rs @@ -250,10 +250,12 @@ pub fn update_spot_balances( } if token_amount > 0 { + msg!("token amount to transfer: {}", token_amount); spot_balance.update_balance_type(*update_direction)?; let round_up = update_direction == &SpotBalanceType::Borrow; let balance_delta = get_spot_balance(token_amount, spot_market, update_direction, round_up)?; + msg!("balance delta {}", balance_delta); spot_balance.increase_balance(balance_delta)?; increase_spot_balance(balance_delta, spot_market, update_direction)?; } diff --git a/programs/drift/src/controller/token.rs b/programs/drift/src/controller/token.rs index 81aa3997a0..201c0ea737 100644 --- a/programs/drift/src/controller/token.rs +++ b/programs/drift/src/controller/token.rs @@ -9,7 +9,7 @@ use anchor_spl::token_2022::spl_token_2022::extension::{ }; use anchor_spl::token_2022::spl_token_2022::state::Mint as MintInner; use anchor_spl::token_interface::{ - self, CloseAccount, Mint, TokenAccount, TokenInterface, Transfer, TransferChecked, + self, Burn, CloseAccount, Mint, MintTo, TokenAccount, TokenInterface, Transfer, TransferChecked, }; use std::iter::Peekable; use std::slice::Iter; @@ -25,7 +25,31 @@ pub fn send_from_program_vault<'info>( remaining_accounts: Option<&mut Peekable>>>, ) -> Result<()> { let signature_seeds = get_signer_seeds(&nonce); - let signers = &[&signature_seeds[..]]; + + send_from_program_vault_with_signature_seeds( + token_program, + from, + to, + authority, + &signature_seeds, + amount, + mint, + remaining_accounts, + ) +} + +#[inline] +pub fn send_from_program_vault_with_signature_seeds<'info>( + token_program: &Interface<'info, TokenInterface>, + from: &InterfaceAccount<'info, TokenAccount>, + to: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &Option>, + remaining_accounts: Option<&mut Peekable>>>, +) -> Result<()> { + let signers = &[signature_seeds]; if let Some(mint) = mint { if let Some(remaining_accounts) = remaining_accounts { @@ -137,6 +161,56 @@ pub fn close_vault<'info>( token_interface::close_account(cpi_context) } +pub fn mint_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signers = &[signature_seeds]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = MintTo { + mint: mint_account_info, + to: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::mint_to(cpi_context, amount) +} + +pub fn burn_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signers = &[signature_seeds]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = Burn { + mint: mint_account_info, + from: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::burn(cpi_context, amount) +} + pub fn validate_mint_fee(account_info: &AccountInfo) -> Result<()> { let mint_data = account_info.try_borrow_data()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 61dee9f5f8..f8dcd300db 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; - pub type DriftResult = std::result::Result; #[error_code] @@ -639,6 +638,48 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid Constituent")] + InvalidConstituent, + #[msg("Invalid Amm Constituent Mapping argument")] + InvalidAmmConstituentMappingArgument, + #[msg("Invalid update constituent update target weights argument")] + InvalidUpdateConstituentTargetBaseArgument, + #[msg("Constituent not found")] + ConstituentNotFound, + #[msg("Constituent could not load")] + ConstituentCouldNotLoad, + #[msg("Constituent wrong mutability")] + ConstituentWrongMutability, + #[msg("Wrong number of constituents passed to instruction")] + WrongNumberOfConstituents, + #[msg("Oracle too stale for LP AUM update")] + OracleTooStaleForLPAUMUpdate, + #[msg("Insufficient constituent token balance")] + InsufficientConstituentTokenBalance, + #[msg("Amm Cache data too stale")] + AMMCacheStale, + #[msg("LP Pool AUM not updated recently")] + LpPoolAumDelayed, + #[msg("Constituent oracle is stale")] + ConstituentOracleStale, + #[msg("LP Invariant failed")] + LpInvariantFailed, + #[msg("Invalid constituent derivative weights")] + InvalidConstituentDerivativeWeights, + #[msg("Unauthorized dlp authority")] + UnauthorizedDlpAuthority, + #[msg("Max DLP AUM Breached")] + MaxDlpAumBreached, + #[msg("Settle Lp Pool Disabled")] + SettleLpPoolDisabled, + #[msg("Mint/Redeem Lp Pool Disabled")] + MintRedeemLpPoolDisabled, + #[msg("Settlement amount exceeded")] + LpPoolSettleInvariantBreached, + #[msg("Invalid constituent operation")] + InvalidConstituentOperation, + #[msg("Unauthorized for operation")] + Unauthorized, } #[macro_export] diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index 0c2a80addb..df2415d936 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -1,3 +1,6 @@ +use anchor_lang::prelude::Pubkey; +use solana_program::pubkey; + pub mod pyth_program { use solana_program::declare_id; #[cfg(feature = "mainnet-beta")] @@ -107,3 +110,8 @@ pub mod amm_spread_adjust_wallet { #[cfg(feature = "anchor-test")] declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); } + +pub mod lp_pool_swap_wallet { + use solana_program::declare_id; + declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); +} diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index a4ecc2f8d1..3fc172c225 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,6 +1,3 @@ -use std::convert::{identity, TryInto}; -use std::mem::size_of; - use crate::{msg, FeatureBitFlags}; use anchor_lang::prelude::*; use anchor_spl::token_2022::Token2022; @@ -9,6 +6,8 @@ use phoenix::quantities::WrapperU64; use pyth_solana_receiver_sdk::cpi::accounts::InitPriceUpdate; use pyth_solana_receiver_sdk::program::PythSolanaReceiver; use serum_dex::state::ToAlignedBytes; +use std::convert::{identity, TryInto}; +use std::mem::size_of; use crate::controller::token::close_vault; use crate::error::ErrorCode; @@ -33,6 +32,7 @@ use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::math::{amm, bn}; use crate::optional_accounts::get_token_mint; +use crate::state::amm_cache::{AmmCache, CacheInfo, AMM_POSITIONS_CACHE}; use crate::state::events::{ CurveRecord, DepositDirection, DepositExplanation, DepositRecord, SpotMarketVaultDepositRecord, }; @@ -65,7 +65,9 @@ use crate::state::spot_market::{ TokenProgramFlag, }; use crate::state::spot_market_map::get_writable_spot_market_set; -use crate::state::state::{ExchangeStatus, FeeStructure, OracleGuardRails, State}; +use crate::state::state::{ + ExchangeStatus, FeeStructure, LpPoolFeatureBitFlags, OracleGuardRails, State, +}; use crate::state::traits::Size; use crate::state::user::{User, UserStats}; use crate::validate; @@ -79,6 +81,7 @@ use crate::{load, FEE_ADJUSTMENT_MAX}; use crate::{load_mut, PTYH_PRICE_FEED_SEED_PREFIX}; use crate::{math, safe_decrement, safe_increment}; use crate::{math_error, SPOT_BALANCE_PRECISION}; + use anchor_spl::token_2022::spl_token_2022::extension::transfer_hook::TransferHook; use anchor_spl::token_2022::spl_token_2022::extension::{ BaseStateWithExtensions, StateWithExtensions, @@ -115,7 +118,8 @@ pub fn handle_initialize(ctx: Context) -> Result<()> { max_number_of_sub_accounts: 0, max_initialize_user_fee: 0, feature_bit_flags: 0, - padding: [0; 9], + lp_pool_feature_bit_flags: 0, + padding: [0; 8], }; Ok(()) @@ -965,7 +969,10 @@ pub fn handle_initialize_perp_market( high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding1: 0, + lp_fee_transfer_scalar: 1, + lp_status: 0, + lp_exchange_fee_excluscion_scalar: 0, + lp_paused_operations: 0, last_fill_price: 0, padding: [0; 24], amm: AMM { @@ -1070,6 +1077,17 @@ pub fn handle_initialize_perp_market( safe_increment!(state.number_of_markets, 1); + let amm_cache = &mut ctx.accounts.amm_cache; + let current_len = amm_cache.cache.len(); + amm_cache + .cache + .resize_with(current_len + 1, CacheInfo::default); + let current_market_info = amm_cache.cache.get_mut(current_len).unwrap(); + current_market_info.slot = clock_slot; + current_market_info.oracle = perp_market.amm.oracle; + current_market_info.oracle_source = u8::from(perp_market.amm.oracle_source); + amm_cache.validate(state)?; + controller::amm::update_concentration_coef(perp_market, concentration_coef_scale)?; crate::dlog!(oracle_price); @@ -1085,6 +1103,95 @@ pub fn handle_initialize_perp_market( Ok(()) } +pub fn handle_initialize_amm_cache(ctx: Context) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let state = &ctx.accounts.state; + amm_cache + .cache + .resize_with(state.number_of_markets as usize, CacheInfo::default); + amm_cache.bump = ctx.bumps.amm_cache; + + Ok(()) +} + +pub fn handle_update_initial_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + + for (_, perp_market_loader) in perp_market_map.0 { + let perp_market = perp_market_loader.load()?; + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + let mm_oracle_data = perp_market.get_mm_oracle_price_data( + *oracle_data, + slot, + &ctx.accounts.state.oracle_guard_rails.validity, + )?; + + amm_cache.update_perp_market_fields(&perp_market)?; + amm_cache.update_oracle_info( + slot, + perp_market.market_index, + &mm_oracle_data, + &perp_market, + &state.oracle_guard_rails, + )?; + } + + Ok(()) +} +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +pub struct OverrideAmmCacheParams { + pub quote_owed_from_lp_pool: Option, + pub last_settle_slot: Option, + pub last_fee_pool_token_amount: Option, + pub last_net_pnl_pool_token_amount: Option, +} + +pub fn handle_override_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + market_index: u16, + override_params: OverrideAmmCacheParams, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + + let cache_entry = amm_cache.cache.get_mut(market_index as usize); + if cache_entry.is_none() { + msg!("No cache entry found for market index {}", market_index); + return Ok(()); + } + + let cache_entry = cache_entry.unwrap(); + if let Some(quote_owed_from_lp_pool) = override_params.quote_owed_from_lp_pool { + cache_entry.quote_owed_from_lp_pool = quote_owed_from_lp_pool; + } + if let Some(last_settle_slot) = override_params.last_settle_slot { + cache_entry.last_settle_slot = last_settle_slot; + } + if let Some(last_fee_pool_token_amount) = override_params.last_fee_pool_token_amount { + cache_entry.last_fee_pool_token_amount = last_fee_pool_token_amount; + } + if let Some(last_net_pnl_pool_token_amount) = override_params.last_net_pnl_pool_token_amount { + cache_entry.last_net_pnl_pool_token_amount = last_net_pnl_pool_token_amount; + } + + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -1942,6 +2049,12 @@ pub fn handle_deposit_into_perp_market_fee_pool<'c: 'info, 'info>( let quote_spot_market = &mut load_mut!(ctx.accounts.quote_spot_market)?; + controller::spot_balance::update_spot_market_cumulative_interest( + &mut *quote_spot_market, + None, + Clock::get()?.unix_timestamp, + )?; + controller::spot_balance::update_spot_balances( amount.cast::()?, &SpotBalanceType::Deposit, @@ -3271,10 +3384,11 @@ pub fn handle_update_perp_market_paused_operations( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_contract_tier( - ctx: Context, + ctx: Context, contract_tier: ContractTier, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; msg!("perp market {}", perp_market.market_index); msg!( @@ -3284,6 +3398,8 @@ pub fn handle_update_perp_market_contract_tier( ); perp_market.contract_tier = contract_tier; + amm_cache.update_perp_market_fields(perp_market)?; + Ok(()) } @@ -3578,6 +3694,7 @@ pub fn handle_update_perp_market_oracle( skip_invariant_check: bool, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; msg!("perp market {}", perp_market.market_index); let clock = Clock::get()?; @@ -3656,6 +3773,8 @@ pub fn handle_update_perp_market_oracle( perp_market.amm.oracle = oracle; perp_market.amm.oracle_source = oracle_source; + amm_cache.update_perp_market_fields(perp_market)?; + Ok(()) } @@ -3806,6 +3925,40 @@ pub fn handle_update_perp_market_min_order_size( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_market_lp_pool_fee_transfer_scalar( + ctx: Context, + optional_lp_fee_transfer_scalar: Option, + optional_lp_net_pnl_transfer_scalar: Option, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + + if let Some(lp_fee_transfer_scalar) = optional_lp_fee_transfer_scalar { + msg!( + "perp_market.: {:?} -> {:?}", + perp_market.lp_fee_transfer_scalar, + lp_fee_transfer_scalar + ); + + perp_market.lp_fee_transfer_scalar = lp_fee_transfer_scalar; + } + + if let Some(lp_net_pnl_transfer_scalar) = optional_lp_net_pnl_transfer_scalar { + msg!( + "perp_market.: {:?} -> {:?}", + perp_market.lp_exchange_fee_excluscion_scalar, + lp_net_pnl_transfer_scalar + ); + + perp_market.lp_exchange_fee_excluscion_scalar = lp_net_pnl_transfer_scalar; + } + + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] @@ -4080,6 +4233,16 @@ pub fn handle_update_perp_market_protected_maker_params( Ok(()) } +pub fn handle_update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + perp_market.lp_paused_operations = lp_paused_operations; + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -4606,8 +4769,8 @@ pub fn handle_update_protected_maker_mode_config( ) -> Result<()> { let mut config = load_mut!(ctx.accounts.protected_maker_mode_config)?; - if current_users.is_some() { - config.current_users = current_users.unwrap(); + if let Some(users) = current_users { + config.current_users = users; } config.max_users = max_users; config.reduce_only = reduce_only as u8; @@ -4915,6 +5078,75 @@ pub fn handle_update_feature_bit_flags_median_trigger_price( Ok(()) } +pub fn handle_update_feature_bit_flags_settle_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting first bit to 1, enabling settle LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::SettleLpPool as u8); + } else { + msg!("Setting first bit to 0, disabling settle LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::SettleLpPool as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_swap_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting second bit to 1, enabling swapping with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::SwapLpPool as u8); + } else { + msg!("Setting second bit to 0, disabling swapping with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::SwapLpPool as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_mint_redeem_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting third bit to 1, enabling minting and redeeming with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::MintRedeemLpPool as u8); + } else { + msg!("Setting third bit to 0, disabling minting and redeeming with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::MintRedeemLpPool as u8); + } + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -5154,12 +5386,57 @@ pub struct InitializePerpMarket<'info> { payer = admin )] pub perp_market: AccountLoader<'info, PerpMarket>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + realloc = AmmCache::space(amm_cache.cache.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, /// CHECK: checked in `initialize_perp_market` pub oracle: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct InitializeAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + init, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + space = AmmCache::space(state.number_of_markets as usize), + bump, + payer = admin + )] + pub amm_cache: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct UpdateInitialAmmCacheInfo<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub state: Box>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, +} + #[derive(Accounts)] pub struct DeleteInitializedPerpMarket<'info> { #[account(mut)] @@ -5195,6 +5472,23 @@ pub struct HotAdminUpdatePerpMarket<'info> { pub perp_market: AccountLoader<'info, PerpMarket>, } +#[derive(Accounts)] +pub struct AdminUpdatePerpMarketContractTier<'info> { + pub admin: Signer<'info>, + #[account( + has_one = admin + )] + pub state: Box>, + #[account(mut)] + pub perp_market: AccountLoader<'info, PerpMarket>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, +} + #[derive(Accounts)] pub struct AdminUpdatePerpMarketAmmSummaryStats<'info> { #[account( @@ -5406,6 +5700,12 @@ pub struct AdminUpdatePerpMarketOracle<'info> { pub oracle: AccountInfo<'info>, /// CHECK: checked in `admin_update_perp_market_oracle` ix constraint pub old_oracle: AccountInfo<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, } #[derive(Accounts)] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 2bf1a003a1..d161b25b62 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -5,6 +5,7 @@ use std::convert::TryFrom; use anchor_lang::prelude::*; use anchor_lang::Discriminator; use anchor_spl::associated_token::get_associated_token_address_with_program_id; +use anchor_spl::token_interface::Mint; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use solana_program::instruction::Instruction; use solana_program::pubkey; @@ -17,23 +18,31 @@ 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::PositionDirection; use crate::controller::spot_balance::update_spot_balances; use crate::controller::token::{receive, send_from_program_vault}; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::admin_hot_wallet; use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_program}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::lp_pool::perp_lp_pool_settlement; use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; +use crate::signer::get_signer_seeds; +use crate::state::amm_cache::CacheInfo; +use crate::state::events::emit_stack; +use crate::state::events::LPSettleRecord; use crate::state::events::{DeleteUserRecord, OrderActionExplanation, SignedMsgOrderRecord}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment_params::drift::MatchFulfillmentParams; @@ -42,8 +51,13 @@ use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; use crate::state::fulfillment_params::serum::SerumFulfillmentParams; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::insurance_fund_stake::InsuranceFundStake; +use crate::state::lp_pool::Constituent; +use crate::state::lp_pool::LPPool; +use crate::state::lp_pool::CONSTITUENT_PDA_SEED; +use crate::state::lp_pool::SETTLE_AMM_ORACLE_MAX_DELAY; use crate::state::oracle_map::OracleMap; use crate::state::order_params::{OrderParams, PlaceOrderOptions}; +use crate::state::paused_operations::PerpLpOperation; use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::{ContractType, MarketStatus, PerpMarket}; use crate::state::perp_market_map::{ @@ -65,11 +79,12 @@ use crate::state::user::{ MarginMode, MarketType, OrderStatus, OrderTriggerCondition, OrderType, User, UserStats, }; use crate::state::user_map::{load_user_map, load_user_maps, UserMap, UserStatsMap}; +use crate::state::zero_copy::AccountZeroCopyMut; +use crate::state::zero_copy::ZeroCopyLoader; use crate::validation::sig_verification::verify_and_decode_ed25519_msg; use crate::validation::user::{validate_user_deletion, validate_user_is_idle}; use crate::{ controller, load, math, print_error, safe_decrement, OracleSource, GOV_SPOT_MARKET_INDEX, - MARGIN_PRECISION, }; use crate::{load_mut, QUOTE_PRECISION_U64}; use crate::{math_error, ID}; @@ -3095,6 +3110,346 @@ pub fn handle_pause_spot_market_deposit_withdraw( Ok(()) } +// Refactored main function +pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, +) -> Result<()> { + use perp_lp_pool_settlement::*; + + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + let now = Clock::get()?.unix_timestamp; + + if !state.allow_settle_lp_pool() { + msg!("settle lp pool disabled"); + return Err(ErrorCode::SettleLpPoolDisabled.into()); + } + + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + let quote_market = &mut ctx.accounts.quote_market.load_mut()?; + let mut quote_constituent = ctx.accounts.constituent.load_mut()?; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map: _, + oracle_map: _, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + slot, + None, + )?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut *quote_market, + None, + now, + )?; + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let mut perp_market = perp_market_loader.load_mut()?; + if perp_market.lp_status == 0 + || PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::SettleQuoteOwed, + ) + { + continue; + } + + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + // Early validation checks + if slot.saturating_sub(cached_info.oracle_slot) > SETTLE_AMM_ORACLE_MAX_DELAY { + msg!( + "Skipping settling perp market {} to dlp because oracle slot is not up to date", + perp_market.market_index + ); + continue; + } + + validate_market_within_price_band(&perp_market, state, cached_info.oracle_price)?; + + if perp_market.is_operation_paused(PerpOperation::SettlePnl) { + msg!( + "Cannot settle pnl under current market = {} status", + perp_market.market_index + ); + continue; + } + + if cached_info.slot != slot { + msg!("Skipping settling perp market {} to lp pool because amm cache was not updated in the same slot", + perp_market.market_index); + return Err(ErrorCode::AMMCacheStale.into()); + } + + // Create settlement context + let settlement_ctx = SettlementContext { + quote_owed_from_lp: cached_info.quote_owed_from_lp_pool, + quote_constituent_token_balance: quote_constituent.vault_token_balance, + fee_pool_balance: get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + pnl_pool_balance: get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + quote_market, + max_settle_quote_amount: lp_pool.max_settle_quote_amount, + }; + + // Calculate settlement + let settlement_result = calculate_settlement_amount(&settlement_ctx)?; + validate_settlement_amount(&settlement_ctx, &settlement_result)?; + + if settlement_result.direction == SettlementDirection::None { + continue; + } + + // Execute token transfer + match settlement_result.direction { + SettlementDirection::FromLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + &ctx.accounts.constituent_quote_token_account, + &ctx.accounts.quote_token_vault, + &ctx.accounts + .constituent_quote_token_account + .to_account_info(), + &Constituent::get_vault_signer_seeds( + "e_constituent.lp_pool, + "e_constituent.spot_market_index, + "e_constituent.vault_bump, + ), + settlement_result.amount_transferred, + Some(remaining_accounts_iter), + )?; + } + SettlementDirection::ToLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + &ctx.accounts.quote_token_vault, + &ctx.accounts.constituent_quote_token_account, + &ctx.accounts.drift_signer, + &get_signer_seeds(&state.signer_nonce), + settlement_result.amount_transferred, + Some(remaining_accounts_iter), + )?; + } + SettlementDirection::None => unreachable!(), + } + + // Update market pools + update_perp_market_pools_and_quote_market_balance( + &mut perp_market, + &settlement_result, + quote_market, + )?; + + // Emit settle event + let record_id = get_then_update_id!(lp_pool, settle_id); + emit!(LPSettleRecord { + record_id, + last_ts: cached_info.last_settle_ts, + last_slot: cached_info.last_settle_slot, + slot, + ts: now, + perp_market_index: perp_market.market_index, + settle_to_lp_amount: match settlement_result.direction { + SettlementDirection::FromLpPool => settlement_result + .amount_transferred + .cast::()? + .saturating_mul(-1), + SettlementDirection::ToLpPool => + settlement_result.amount_transferred.cast::()?, + SettlementDirection::None => unreachable!(), + }, + perp_amm_pnl_delta: cached_info + .last_net_pnl_pool_token_amount + .safe_sub(cached_info.last_settle_amm_pnl)? + .cast::()?, + perp_amm_ex_fee_delta: cached_info + .last_exchange_fees + .safe_sub(cached_info.last_settle_amm_ex_fees)? + .cast::()?, + lp_aum: lp_pool.last_aum, + lp_price: lp_pool.get_price(lp_pool.token_supply)?, + }); + + // Calculate new quote owed amount + let new_quote_owed = match settlement_result.direction { + SettlementDirection::FromLpPool => cached_info + .quote_owed_from_lp_pool + .safe_sub(settlement_result.amount_transferred as i64)?, + SettlementDirection::ToLpPool => cached_info + .quote_owed_from_lp_pool + .safe_add(settlement_result.amount_transferred as i64)?, + SettlementDirection::None => cached_info.quote_owed_from_lp_pool, + }; + + // Update cache info + update_cache_info(cached_info, &settlement_result, new_quote_owed, slot, now)?; + + // Update LP pool stats + match settlement_result.direction { + SettlementDirection::FromLpPool => { + lp_pool.cumulative_quote_sent_to_perp_markets = lp_pool + .cumulative_quote_sent_to_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::ToLpPool => { + lp_pool.cumulative_quote_received_from_perp_markets = lp_pool + .cumulative_quote_received_from_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::None => {} + } + + // Sync constituent token balance + let constituent_token_account = &mut ctx.accounts.constituent_quote_token_account; + constituent_token_account.reload()?; + quote_constituent.sync_token_balance(constituent_token_account.amount); + } + + // Final validation + ctx.accounts.quote_token_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + quote_market, + ctx.accounts.quote_token_vault.amount, + )?; + + Ok(()) +} + +pub fn handle_update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, +) -> Result<()> { + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + + let state = &ctx.accounts.state; + let quote_market = ctx.accounts.quote_market.load()?; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + let slot = Clock::get()?.slot; + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let perp_market = perp_market_loader.load()?; + if perp_market.lp_status == 0 { + continue; + } + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + validate!( + perp_market.oracle_id() == cached_info.oracle_id()?, + ErrorCode::DefaultError, + "oracle id mismatch between amm cache and perp market" + )?; + + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + let mm_oracle_price_data = perp_market.get_mm_oracle_price_data( + *oracle_data, + slot, + &ctx.accounts.state.oracle_guard_rails.validity, + )?; + + cached_info.update_perp_market_fields(&perp_market)?; + cached_info.update_oracle_info( + slot, + &mm_oracle_price_data, + &perp_market, + &state.oracle_guard_rails, + )?; + + if perp_market.lp_status != 0 + && !PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::TrackAmmRevenue, + ) + { + amm_cache.update_amount_owed_from_lp_pool(&perp_market, "e_market)?; + } + } + + Ok(()) +} + +#[derive(Accounts)] +pub struct SettleAmmPnlToLp<'info> { + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account(mut)] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + mut, + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + owner = crate::ID, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump = constituent.load()?.bump, + constraint = constituent.load()?.mint.eq("e_market.load()?.mint) + )] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + )] + pub constituent_quote_token_account: Box>, + #[account( + mut, + address = quote_market.load()?.vault, + token::authority = drift_signer, + )] + pub quote_token_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateAmmCache<'info> { + #[account(mut)] + pub keeper: Signer<'info>, + pub state: Box>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, +} + #[derive(Accounts)] pub struct FillOrder<'info> { pub state: Box>, diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs new file mode 100644 index 0000000000..081bde1604 --- /dev/null +++ b/programs/drift/src/instructions/lp_admin.rs @@ -0,0 +1,1257 @@ +use crate::{controller, load_mut}; +use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; +use crate::error::ErrorCode; +use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet}; +use crate::instructions::optional_accounts::get_token_mint; +use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; +use crate::math::safe_math::SafeMath; +use crate::state::amm_cache::AmmCache; +use crate::state::lp_pool::{ + AmmConstituentDatum, AmmConstituentMapping, Constituent, ConstituentCorrelations, + ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, + CONSTITUENT_CORRELATIONS_PDA_SEED, CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_BASE_PDA_SEED, + CONSTITUENT_VAULT_PDA_SEED, +}; +use crate::state::perp_market::PerpMarket; +use crate::state::spot_market::SpotMarket; +use crate::state::state::State; +use crate::validate; +use anchor_lang::prelude::*; +use anchor_lang::Discriminator; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::Token; +use anchor_spl::token_2022::Token2022; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::ids::{ + jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, lighthouse, marinade_mainnet, + serum_program, +}; + +use crate::state::traits::Size; +use solana_program::sysvar::instructions; + +use super::optional_accounts::get_token_interface; + +pub fn handle_initialize_lp_pool( + ctx: Context, + name: [u8; 32], + min_mint_fee: i64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + whitelist_mint: Pubkey, +) -> Result<()> { + let lp_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_init()?; + let mint = &ctx.accounts.mint; + + validate!( + mint.decimals == 6, + ErrorCode::DefaultError, + "lp mint must have 6 decimals" + )?; + + validate!( + mint.mint_authority == Some(lp_key).into(), + ErrorCode::DefaultError, + "lp mint must have drift_signer as mint authority" + )?; + + *lp_pool = LPPool { + name, + pubkey: ctx.accounts.lp_pool.key(), + mint: mint.key(), + constituent_target_base: ctx.accounts.constituent_target_base.key(), + constituent_correlations: ctx.accounts.constituent_correlations.key(), + constituents: 0, + max_aum, + last_aum: 0, + last_aum_slot: 0, + max_settle_quote_amount: max_settle_quote_amount_per_market, + last_hedge_ts: 0, + total_mint_redeem_fees_paid: 0, + bump: ctx.bumps.lp_pool, + min_mint_fee, + token_supply: 0, + mint_redeem_id: 1, + settle_id: 1, + quote_consituent_index: 0, + cumulative_quote_sent_to_perp_markets: 0, + cumulative_quote_received_from_perp_markets: 0, + gamma_execution: 2, + volatility: 4, + xi: 2, + padding: 0, + whitelist_mint, + }; + + let amm_constituent_mapping = &mut ctx.accounts.amm_constituent_mapping; + amm_constituent_mapping.lp_pool = ctx.accounts.lp_pool.key(); + amm_constituent_mapping.bump = ctx.bumps.amm_constituent_mapping; + amm_constituent_mapping + .weights + .resize_with(0 as usize, AmmConstituentDatum::default); + amm_constituent_mapping.validate()?; + + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + constituent_target_base.lp_pool = ctx.accounts.lp_pool.key(); + constituent_target_base.bump = ctx.bumps.constituent_target_base; + constituent_target_base + .targets + .resize_with(0 as usize, TargetsDatum::default); + constituent_target_base.validate()?; + + let consituent_correlations = &mut ctx.accounts.constituent_correlations; + consituent_correlations.lp_pool = ctx.accounts.lp_pool.key(); + consituent_correlations.bump = ctx.bumps.constituent_correlations; + consituent_correlations.correlations.resize(0 as usize, 0); + consituent_correlations.validate()?; + + Ok(()) +} + +pub fn handle_increase_lp_pool_max_aum( + ctx: Context, + new_max_aum: u128, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + msg!( + "lp pool max aum: {:?} -> {:?}", + lp_pool.max_aum, + new_max_aum + ); + lp_pool.max_aum = new_max_aum; + Ok(()) +} + +pub fn handle_initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade_bps: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_init()?; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + let current_len = constituent_target_base.targets.len(); + + constituent_target_base + .targets + .resize_with((current_len + 1) as usize, TargetsDatum::default); + + let new_target = constituent_target_base + .targets + .get_mut(current_len) + .unwrap(); + new_target.cost_to_trade_bps = cost_to_trade_bps; + constituent_target_base.validate()?; + + msg!( + "initializing constituent {} with spot market index {}", + lp_pool.constituents, + spot_market_index + ); + + validate!( + derivative_weight <= PRICE_PRECISION_U64, + ErrorCode::InvalidConstituent, + "stablecoin_weight must be between 0 and 1", + )?; + + if let Some(constituent_derivative_index) = constituent_derivative_index { + validate!( + constituent_derivative_index < lp_pool.constituents as i16, + ErrorCode::InvalidConstituent, + "constituent_derivative_index must be less than lp_pool.constituents" + )?; + } + + constituent.spot_market_index = spot_market_index; + constituent.constituent_index = lp_pool.constituents; + constituent.decimals = decimals; + constituent.max_weight_deviation = max_weight_deviation; + constituent.swap_fee_min = swap_fee_min; + constituent.swap_fee_max = swap_fee_max; + constituent.oracle_staleness_threshold = oracle_staleness_threshold; + constituent.pubkey = ctx.accounts.constituent.key(); + constituent.mint = ctx.accounts.spot_market_mint.key(); + constituent.vault = ctx.accounts.constituent_vault.key(); + constituent.bump = ctx.bumps.constituent; + constituent.vault_bump = ctx.bumps.constituent_vault; + constituent.max_borrow_token_amount = max_borrow_token_amount; + constituent.lp_pool = lp_pool.pubkey; + constituent.constituent_index = (constituent_target_base.targets.len() - 1) as u16; + constituent.next_swap_id = 1; + constituent.constituent_derivative_index = constituent_derivative_index.unwrap_or(-1); + constituent.constituent_derivative_depeg_threshold = constituent_derivative_depeg_threshold; + constituent.derivative_weight = derivative_weight; + constituent.volatility = volatility; + constituent.gamma_execution = gamma_execution; + constituent.gamma_inventory = gamma_inventory; + constituent.spot_balance.market_index = spot_market_index; + constituent.xi = xi; + lp_pool.constituents += 1; + + if constituent.spot_market_index == QUOTE_SPOT_MARKET_INDEX { + lp_pool.quote_consituent_index = constituent.constituent_index; + } + + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + validate!( + new_constituent_correlations.len() as u16 == lp_pool.constituents - 1, + ErrorCode::InvalidConstituent, + "expected {} correlations, got {}", + lp_pool.constituents, + new_constituent_correlations.len() + )?; + constituent_correlations.add_new_constituent(&new_constituent_correlations)?; + + Ok(()) +} + +pub fn handle_update_constituent_status<'info>( + ctx: Context, + new_status: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent status: {:?} -> {:?}", + constituent.status, + new_status + ); + constituent.status = new_status; + Ok(()) +} + +pub fn handle_update_constituent_paused_operations<'info>( + ctx: Context, + paused_operations: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent paused operations: {:?} -> {:?}", + constituent.paused_operations, + paused_operations + ); + constituent.paused_operations = paused_operations; + Ok(()) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ConstituentParams { + pub max_weight_deviation: Option, + pub swap_fee_min: Option, + pub swap_fee_max: Option, + pub max_borrow_token_amount: Option, + pub oracle_staleness_threshold: Option, + pub cost_to_trade_bps: Option, + pub constituent_derivative_index: Option, + pub derivative_weight: Option, + pub volatility: Option, + pub gamma_execution: Option, + pub gamma_inventory: Option, + pub xi: Option, +} + +pub fn handle_update_constituent_params<'info>( + ctx: Context, + constituent_params: ConstituentParams, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + if constituent.spot_balance.market_index != constituent.spot_market_index { + constituent.spot_balance.market_index = constituent.spot_market_index; + } + + if let Some(max_weight_deviation) = constituent_params.max_weight_deviation { + msg!( + "max_weight_deviation: {:?} -> {:?}", + constituent.max_weight_deviation, + max_weight_deviation + ); + constituent.max_weight_deviation = max_weight_deviation; + } + + if let Some(swap_fee_min) = constituent_params.swap_fee_min { + msg!( + "swap_fee_min: {:?} -> {:?}", + constituent.swap_fee_min, + swap_fee_min + ); + constituent.swap_fee_min = swap_fee_min; + } + + if let Some(swap_fee_max) = constituent_params.swap_fee_max { + msg!( + "swap_fee_max: {:?} -> {:?}", + constituent.swap_fee_max, + swap_fee_max + ); + constituent.swap_fee_max = swap_fee_max; + } + + if let Some(oracle_staleness_threshold) = constituent_params.oracle_staleness_threshold { + msg!( + "oracle_staleness_threshold: {:?} -> {:?}", + constituent.oracle_staleness_threshold, + oracle_staleness_threshold + ); + constituent.oracle_staleness_threshold = oracle_staleness_threshold; + } + + if let Some(cost_to_trade_bps) = constituent_params.cost_to_trade_bps { + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + + let target = constituent_target_base + .targets + .get_mut(constituent.constituent_index as usize) + .unwrap(); + + msg!( + "cost_to_trade: {:?} -> {:?}", + target.cost_to_trade_bps, + cost_to_trade_bps + ); + target.cost_to_trade_bps = cost_to_trade_bps; + } + + if let Some(derivative_weight) = constituent_params.derivative_weight { + msg!( + "derivative_weight: {:?} -> {:?}", + constituent.derivative_weight, + derivative_weight + ); + constituent.derivative_weight = derivative_weight; + } + + if let Some(constituent_derivative_index) = constituent_params.constituent_derivative_index { + msg!( + "constituent_derivative_index: {:?} -> {:?}", + constituent.constituent_derivative_index, + constituent_derivative_index + ); + constituent.constituent_derivative_index = constituent_derivative_index; + } + + if let Some(gamma_execution) = constituent_params.gamma_execution { + msg!( + "gamma_execution: {:?} -> {:?}", + constituent.gamma_execution, + gamma_execution + ); + constituent.gamma_execution = gamma_execution; + } + + if let Some(gamma_inventory) = constituent_params.gamma_inventory { + msg!( + "gamma_inventory: {:?} -> {:?}", + constituent.gamma_inventory, + gamma_inventory + ); + constituent.gamma_inventory = gamma_inventory; + } + + if let Some(xi) = constituent_params.xi { + msg!("xi: {:?} -> {:?}", constituent.xi, xi); + constituent.xi = xi; + } + + if let Some(max_borrow_token_amount) = constituent_params.max_borrow_token_amount { + msg!( + "max_borrow_token_amount: {:?} -> {:?}", + constituent.max_borrow_token_amount, + max_borrow_token_amount + ); + constituent.max_borrow_token_amount = max_borrow_token_amount; + } + + if let Some(volatility) = constituent_params.volatility { + msg!( + "volatility: {:?} -> {:?}", + constituent.volatility, + volatility + ); + constituent.volatility = volatility; + } + + Ok(()) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct LpPoolParams { + pub max_settle_quote_amount: Option, + pub volatility: Option, + pub gamma_execution: Option, + pub xi: Option, + pub whitelist_mint: Option, +} + +pub fn handle_update_lp_pool_params<'info>( + ctx: Context, + lp_pool_params: LpPoolParams, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + if let Some(max_settle_quote_amount) = lp_pool_params.max_settle_quote_amount { + msg!( + "max_settle_quote_amount: {:?} -> {:?}", + lp_pool.max_settle_quote_amount, + max_settle_quote_amount + ); + lp_pool.max_settle_quote_amount = max_settle_quote_amount; + } + + if let Some(volatility) = lp_pool_params.volatility { + msg!("volatility: {:?} -> {:?}", lp_pool.volatility, volatility); + lp_pool.volatility = volatility; + } + + if let Some(gamma_execution) = lp_pool_params.gamma_execution { + msg!( + "gamma_execution: {:?} -> {:?}", + lp_pool.gamma_execution, + gamma_execution + ); + lp_pool.gamma_execution = gamma_execution; + } + + if let Some(xi) = lp_pool_params.xi { + msg!("xi: {:?} -> {:?}", lp_pool.xi, xi); + lp_pool.xi = xi; + } + + if let Some(whitelist_mint) = lp_pool_params.whitelist_mint { + msg!( + "whitelist_mint: {:?} -> {:?}", + lp_pool.whitelist_mint, + whitelist_mint + ); + lp_pool.whitelist_mint = whitelist_mint; + } + + Ok(()) +} + +pub fn handle_update_amm_constituent_mapping_data<'info>( + ctx: Context, + amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + for datum in amm_constituent_mapping_data { + let existing_datum = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == datum.perp_market_index + && existing_datum.constituent_index == datum.constituent_index + }); + + if existing_datum.is_none() { + msg!( + "AmmConstituentDatum not found for perp_market_index {} and constituent_index {}", + datum.perp_market_index, + datum.constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights[existing_datum.unwrap()] = AmmConstituentDatum { + perp_market_index: datum.perp_market_index, + constituent_index: datum.constituent_index, + weight: datum.weight, + last_slot: Clock::get()?.slot, + ..AmmConstituentDatum::default() + }; + + msg!( + "Updated AmmConstituentDatum for perp_market_index {} and constituent_index {} to {}", + datum.perp_market_index, + datum.constituent_index, + datum.weight + ); + } + + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_remove_amm_constituent_mapping_data<'info>( + ctx: Context, + perp_market_index: u16, + constituent_index: u16, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + let position = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == perp_market_index + && existing_datum.constituent_index == constituent_index + }); + + if position.is_none() { + msg!( + "Not found for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights.remove(position.unwrap()); + amm_mapping.weights.shrink_to_fit(); + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_add_amm_constituent_data<'info>( + ctx: Context, + init_amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + let constituent_target_base = &ctx.accounts.constituent_target_base; + let state = &ctx.accounts.state; + let mut current_len = amm_mapping.weights.len(); + + for init_datum in init_amm_constituent_mapping_data { + let perp_market_index = init_datum.perp_market_index; + + validate!( + perp_market_index < state.number_of_markets, + ErrorCode::InvalidAmmConstituentMappingArgument, + "perp_market_index too large compared to number of markets" + )?; + + validate!( + (init_datum.constituent_index as usize) < constituent_target_base.targets.len(), + ErrorCode::InvalidAmmConstituentMappingArgument, + "constituent_index too large compared to number of constituents in target weights" + )?; + + let constituent_index = init_datum.constituent_index; + let mut datum = AmmConstituentDatum::default(); + datum.perp_market_index = perp_market_index; + datum.constituent_index = constituent_index; + datum.weight = init_datum.weight; + datum.last_slot = Clock::get()?.slot; + + // Check if the datum already exists + let exists = amm_mapping.weights.iter().any(|d| { + d.perp_market_index == perp_market_index && d.constituent_index == constituent_index + }); + + validate!( + !exists, + ErrorCode::InvalidAmmConstituentMappingArgument, + "AmmConstituentDatum already exists for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + )?; + + // Add the new datum to the mapping + current_len += 1; + amm_mapping.weights.resize(current_len, datum); + } + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_update_constituent_correlation_data<'info>( + ctx: Context, + index1: u16, + index2: u16, + corr: i64, +) -> Result<()> { + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + constituent_correlations.set_correlation(index1, index2, corr)?; + + msg!( + "Updated correlation between constituent {} and {} to {}", + index1, + index2, + corr + ); + + constituent_correlations.validate()?; + + Ok(()) +} + +pub fn handle_begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, +) -> Result<()> { + // Check admin + let state = &ctx.accounts.state; + let admin = &ctx.accounts.admin; + #[cfg(feature = "anchor-test")] + validate!( + admin.key() == admin_hot_wallet::id() || admin.key() == state.admin, + ErrorCode::Unauthorized, + "Wrong signer for lp taker swap" + )?; + #[cfg(not(feature = "anchor-test"))] + validate!( + admin.key() == lp_pool_swap_wallet::id(), + ErrorCode::DefaultError, + "Wrong signer for lp taker swap" + )?; + + let ixs = ctx.accounts.instructions.as_ref(); + let current_index = instructions::load_current_index_checked(ixs)? as usize; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let current_ix = instructions::load_instruction_at_checked(current_index, ixs)?; + validate!( + current_ix.program_id == *ctx.program_id, + ErrorCode::InvalidSwap, + "SwapBegin must be a top-level instruction (cant be cpi)" + )?; + + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSwap, + "in and out market the same" + )?; + + validate!( + amount_in != 0, + ErrorCode::InvalidSwap, + "amount_in cannot be zero" + )?; + + // Make sure we have enough balance to do the swap + let constituent_in_token_account = &ctx.accounts.constituent_in_token_account; + + msg!("amount_in: {}", amount_in); + msg!( + "constituent_in_token_account.amount: {}", + constituent_in_token_account.amount + ); + validate!( + amount_in <= constituent_in_token_account.amount, + ErrorCode::InvalidSwap, + "trying to swap more than the balance of the constituent in token account" + )?; + + validate!( + out_constituent.flash_loan_initial_token_amount == 0, + ErrorCode::InvalidSwap, + "begin_lp_swap ended in invalid state" + )?; + + in_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_in_token_account.amount; + out_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_out_token_account.amount; + + // drop(in_constituent); + // drop(out_constituent); + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + constituent_in_token_account, + &ctx.accounts.signer_in_token_account, + &constituent_in_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &in_constituent.lp_pool, + &in_constituent.spot_market_index, + &in_constituent.vault_bump, + ), + amount_in, + &mint, + Some(remaining_accounts_iter), + )?; + + drop(in_constituent); + + // The only other drift program allowed is SwapEnd + let mut index = current_index + 1; + let mut found_end = false; + loop { + let ix = match instructions::load_instruction_at_checked(index, ixs) { + Ok(ix) => ix, + Err(ProgramError::InvalidArgument) => break, + Err(e) => return Err(e.into()), + }; + + // Check that the drift program key is not used + if ix.program_id == crate::id() { + // must be the last ix -- this could possibly be relaxed + validate!( + !found_end, + ErrorCode::InvalidSwap, + "the transaction must not contain a Drift instruction after FlashLoanEnd" + )?; + found_end = true; + + // must be the SwapEnd instruction + let discriminator = crate::instruction::EndLpSwap::discriminator(); + validate!( + ix.data[0..8] == discriminator, + ErrorCode::InvalidSwap, + "last drift ix must be end of swap" + )?; + + validate!( + ctx.accounts.signer_out_token_account.key() == ix.accounts[2].pubkey, + ErrorCode::InvalidSwap, + "the out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.signer_in_token_account.key() == ix.accounts[3].pubkey, + ErrorCode::InvalidSwap, + "the in_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_out_token_account.key() == ix.accounts[4].pubkey, + ErrorCode::InvalidSwap, + "the constituent out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_in_token_account.key() == ix.accounts[5].pubkey, + ErrorCode::InvalidSwap, + "the constituent in token account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.out_constituent.key() == ix.accounts[6].pubkey, + ErrorCode::InvalidSwap, + "the out constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.in_constituent.key() == ix.accounts[7].pubkey, + ErrorCode::InvalidSwap, + "the in constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.lp_pool.key() == ix.accounts[8].pubkey, + ErrorCode::InvalidSwap, + "the lp pool passed to SwapBegin and End must match" + )?; + } else { + if found_end { + if ix.program_id == lighthouse::ID { + continue; + } + + for meta in ix.accounts.iter() { + validate!( + meta.is_writable == false, + ErrorCode::InvalidSwap, + "instructions after swap end must not have writable accounts" + )?; + } + } else { + let mut whitelisted_programs = vec![ + serum_program::id(), + AssociatedToken::id(), + jupiter_mainnet_3::ID, + jupiter_mainnet_4::ID, + jupiter_mainnet_6::ID, + ]; + whitelisted_programs.push(Token::id()); + whitelisted_programs.push(Token2022::id()); + whitelisted_programs.push(marinade_mainnet::ID); + + validate!( + whitelisted_programs.contains(&ix.program_id), + ErrorCode::InvalidSwap, + "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + )?; + + for meta in ix.accounts.iter() { + validate!( + meta.pubkey != crate::id(), + ErrorCode::InvalidSwap, + "instructions between begin and end must not be drift instructions" + )?; + } + } + } + + index += 1; + } + + validate!( + found_end, + ErrorCode::InvalidSwap, + "found no SwapEnd instruction in transaction" + )?; + + Ok(()) +} + +pub fn handle_end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, +) -> Result<()> { + let signer_in_token_account = &ctx.accounts.signer_in_token_account; + let signer_out_token_account = &ctx.accounts.signer_out_token_account; + + let admin_account_info = ctx.accounts.admin.to_account_info(); + + let constituent_in_token_account = &mut ctx.accounts.constituent_in_token_account; + let constituent_out_token_account = &mut ctx.accounts.constituent_out_token_account; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let out_token_program = get_token_interface(remaining_accounts)?; + + let in_mint = get_token_mint(remaining_accounts)?; + let out_mint = get_token_mint(remaining_accounts)?; + + // Residual of what wasnt swapped + if signer_in_token_account.amount > in_constituent.flash_loan_initial_token_amount { + let residual = signer_in_token_account + .amount + .safe_sub(in_constituent.flash_loan_initial_token_amount)?; + + controller::token::receive( + &ctx.accounts.token_program, + signer_in_token_account, + constituent_in_token_account, + &admin_account_info, + residual, + &in_mint, + Some(remaining_accounts), + )?; + } + + // Whatever was swapped + if signer_out_token_account.amount > out_constituent.flash_loan_initial_token_amount { + let residual = signer_out_token_account + .amount + .safe_sub(out_constituent.flash_loan_initial_token_amount)?; + + if let Some(token_interface) = out_token_program { + receive( + &token_interface, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &out_mint, + Some(remaining_accounts), + )?; + } else { + receive( + &ctx.accounts.token_program, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &out_mint, + Some(remaining_accounts), + )?; + } + } + + // Update the balance on the token accounts for after swap + constituent_out_token_account.reload()?; + constituent_in_token_account.reload()?; + out_constituent.sync_token_balance(constituent_out_token_account.amount); + in_constituent.sync_token_balance(constituent_in_token_account.amount); + + out_constituent.flash_loan_initial_token_amount = 0; + in_constituent.flash_loan_initial_token_amount = 0; + + Ok(()) +} + +pub fn handle_update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; + + msg!("perp market {}", perp_market.market_index); + perp_market.lp_status = lp_status; + amm_cache.update_perp_market_fields(&perp_market)?; + + Ok(()) +} + +#[derive(Accounts)] +#[instruction( + name: [u8; 32], +)] +pub struct InitializeLpPool<'info> { + #[account(mut)] + pub admin: Signer<'info>, + #[account( + init, + seeds = [b"lp_pool", name.as_ref()], + space = LPPool::SIZE, + bump, + payer = admin + )] + pub lp_pool: AccountLoader<'info, LPPool>, + pub mint: Account<'info, anchor_spl::token::Mint>, + + #[account( + init, + seeds = [b"LP_POOL_TOKEN_VAULT".as_ref(), lp_pool.key().as_ref()], + bump, + payer = admin, + token::mint = mint, + token::authority = lp_pool + )] + pub lp_pool_token_vault: Box>, + + #[account( + init, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = AmmConstituentMapping::space(0 as usize), + payer = admin, + )] + pub amm_constituent_mapping: Box>, + + #[account( + init, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentTargetBase::space(0 as usize), + payer = admin, + )] + pub constituent_target_base: Box>, + + #[account( + init, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentCorrelations::space(0 as usize), + payer = admin, + )] + pub constituent_correlations: Box>, + + #[account( + has_one = admin + )] + pub state: Box>, + pub token_program: Program<'info, Token>, + + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + spot_market_index: u16, +)] +pub struct InitializeConstituent<'info> { + #[account()] + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + realloc = ConstituentCorrelations::space(constituent_target_base.targets.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_correlations: Box>, + + #[account( + init, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + space = Constituent::SIZE, + payer = admin, + )] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + seeds = [b"spot_market", spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + address = spot_market.load()?.mint + )] + pub spot_market_mint: Box>, + #[account( + init, + seeds = [CONSTITUENT_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + payer = admin, + token::mint = spot_market_mint, + token::authority = constituent_vault + )] + pub constituent_vault: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentParams<'info> { + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + constraint = constituent.load()?.lp_pool == lp_pool.key() + )] + pub constituent_target_base: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentStatus<'info> { + #[account( + mut, + constraint = admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentPausedOperations<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateLpPoolParams<'info> { + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct AddAmmConstituentMappingDatum { + pub constituent_index: u16, + pub perp_market_index: u16, + pub weight: i64, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct AddAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() + amm_constituent_mapping_data.len()), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + pub state: Box>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct UpdateAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct RemoveAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() - 1), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentCorrelation<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + )] + pub constituent_correlations: Box>, + pub state: Box>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPTakerSwap<'info> { + pub state: Box>, + #[account(mut)] + pub admin: Signer<'info>, + /// Signer token accounts + #[account( + mut, + constraint = &constituent_out_token_account.mint.eq(&signer_out_token_account.mint), + token::authority = admin + )] + pub signer_out_token_account: Box>, + #[account( + mut, + constraint = &constituent_in_token_account.mint.eq(&signer_in_token_account.mint), + token::authority = admin + )] + pub signer_in_token_account: Box>, + + /// Constituent token accounts + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + + /// Constituents + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump = out_constituent.load()?.bump, + )] + pub out_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump = in_constituent.load()?.bump, + )] + pub in_constituent: AccountLoader<'info, Constituent>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// Instructions Sysvar for instruction introspection + /// CHECK: fixed instructions sysvar account + #[account(address = instructions::ID)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct UpdatePerpMarketLpPoolStatus<'info> { + pub admin: Signer<'info>, + #[account( + has_one = admin + )] + pub state: Box>, + #[account(mut)] + pub perp_market: AccountLoader<'info, PerpMarket>, + #[account(mut)] + pub amm_cache: Box>, +} diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs new file mode 100644 index 0000000000..884171756d --- /dev/null +++ b/programs/drift/src/instructions/lp_pool.rs @@ -0,0 +1,2007 @@ +use anchor_lang::{prelude::*, Accounts, Key, Result}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::math::constants::{PERCENTAGE_PRECISION, PRICE_PRECISION_I64}; +use crate::math::oracle::OracleValidity; +use crate::state::paused_operations::ConstituentLpOperation; +use crate::validation::whitelist::validate_whitelist_token; +use crate::{ + controller::{ + self, + spot_balance::update_spot_balances, + token::{burn_tokens, mint_tokens}, + }, + error::ErrorCode, + get_then_update_id, + ids::admin_hot_wallet, + math::{ + self, + casting::Cast, + constants::PERCENTAGE_PRECISION_I64, + oracle::{is_oracle_valid_for_action, DriftAction}, + safe_math::SafeMath, + }, + math_error, msg, safe_decrement, safe_increment, + state::{ + amm_cache::{AmmCacheFixed, CacheInfo, AMM_POSITIONS_CACHE}, + constituent_map::{ConstituentMap, ConstituentSet}, + events::{emit_stack, LPMintRedeemRecord, LPSettleRecord, LPSwapRecord}, + lp_pool::{ + update_constituent_target_base_for_derivatives, AmmConstituentDatum, + AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, + ConstituentTargetBaseFixed, LPPool, TargetsDatum, LP_POOL_SWAP_AUM_UPDATE_DELAY, + MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC, + MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC, + }, + oracle_map::OracleMap, + perp_market_map::MarketSet, + spot_market::{SpotBalanceType, SpotMarket}, + spot_market_map::get_writable_spot_market_set_from_many, + state::State, + traits::Size, + user::MarketType, + zero_copy::{AccountZeroCopy, AccountZeroCopyMut, ZeroCopyLoader}, + }, + validate, +}; +use std::convert::TryFrom; +use std::iter::Peekable; +use std::slice::Iter; + +use solana_program::sysvar::clock::Clock; + +use super::optional_accounts::{get_whitelist_token, load_maps, AccountMaps}; +use crate::controller::spot_balance::update_spot_market_cumulative_interest; +use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; +use crate::instructions::constraints::*; +use crate::state::lp_pool::{ + AmmInventoryAndPrices, ConstituentIndexAndDecimalAndPrice, CONSTITUENT_PDA_SEED, + LP_POOL_TOKEN_VAULT_PDA_SEED, +}; + +pub fn handle_update_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, +) -> Result<()> { + let slot = Clock::get()?.slot; + + let lp_pool_key: &Pubkey = &ctx.accounts.lp_pool.key(); + let lp_pool = ctx.accounts.lp_pool.load()?; + let constituent_target_base_key: &Pubkey = &ctx.accounts.constituent_target_base.key(); + + let amm_cache: AccountZeroCopy<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc()?; + + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let amm_constituent_mapping: AccountZeroCopy< + '_, + AmmConstituentDatum, + AmmConstituentMappingFixed, + > = ctx.accounts.amm_constituent_mapping.load_zc()?; + validate!( + amm_constituent_mapping.fixed.lp_pool.eq(lp_pool_key), + ErrorCode::InvalidPDA, + "Amm constituent mapping lp pool pubkey does not match lp pool pubkey", + )?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool_key, remaining_accounts)?; + + let mut amm_inventories: Vec = + Vec::with_capacity(amm_cache.len() as usize); + for (idx, cache_info) in amm_cache.iter().enumerate() { + if cache_info.lp_status_for_perp_market == 0 { + continue; + } + if !is_oracle_valid_for_action( + OracleValidity::try_from(cache_info.oracle_validity)?, + Some(DriftAction::UpdateLpConstituentTargetBase), + )? { + msg!( + "Oracle data for perp market {} is invalid. Skipping update", + idx, + ); + continue; + } + + if slot.safe_sub(cache_info.slot)? > MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC { + msg!("Amm cache for perp market {}. Skipping update", idx); + continue; + } + + if slot.safe_sub(cache_info.oracle_slot)? > MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC { + msg!( + "Amm cache oracle for perp market {} is stale. Skipping update", + idx + ); + continue; + } + + amm_inventories.push(AmmInventoryAndPrices { + inventory: cache_info.position, + price: cache_info.oracle_price, + }); + } + + if amm_inventories.is_empty() { + msg!("No valid inventories found for constituent target weights update"); + return Ok(()); + } + + let mut constituent_indexes_and_decimals_and_prices: Vec = + Vec::with_capacity(constituent_map.0.len()); + for (index, loader) in &constituent_map.0 { + let constituent_ref = loader.load()?; + constituent_indexes_and_decimals_and_prices.push(ConstituentIndexAndDecimalAndPrice { + constituent_index: *index, + decimals: constituent_ref.decimals, + price: constituent_ref.last_oracle_price, + }); + } + + constituent_target_base.update_target_base( + &amm_constituent_mapping, + amm_inventories.as_slice(), + constituent_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + )?; + + Ok(()) +} + +pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + let state = &ctx.accounts.state; + + let slot = Clock::get()?.slot; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + oracle_map: _, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool.pubkey, remaining_accounts)?; + + validate!( + constituent_map.0.len() == lp_pool.constituents as usize, + ErrorCode::WrongNumberOfConstituents, + "Constituent map length does not match lp pool constituent count" + )?; + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool.pubkey) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let amm_cache: AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc_mut()?; + + let (aum, crypto_delta, derivative_groups) = lp_pool.update_aum( + slot, + &constituent_map, + &spot_market_map, + &constituent_target_base, + &amm_cache, + )?; + + // Set USDC stable weight + let total_stable_target_base = aum + .cast::()? + .safe_sub(crypto_delta.abs())? + .max(0_i128); + constituent_target_base + .get_mut(lp_pool.quote_consituent_index as u32) + .target_base = total_stable_target_base.cast::()?; + + msg!( + "stable target base: {}", + constituent_target_base + .get(lp_pool.quote_consituent_index as u32) + .target_base + ); + msg!("aum: {}, crypto_delta: {}", aum, crypto_delta); + msg!("derivative groups: {:?}", derivative_groups); + + update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + )?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + validate!( + state.allow_swap_lp_pool(), + ErrorCode::DefaultError, + "Swapping with LP Pool is disabled" + )?; + + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSpotMarketAccount, + "In and out spot market indices cannot be the same" + )?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let lp_pool = &ctx.accounts.lp_pool.load()?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let constituent_correlations_key = &ctx.accounts.constituent_correlations.key(); + let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = + ctx.accounts.constituent_correlations.load_zc()?; + validate!( + constituent_correlations.fixed.lp_pool.eq(&lp_pool_key) + && constituent_correlations_key.eq(&lp_pool.constituent_correlations), + ErrorCode::InvalidPDA, + "Constituent correlations lp pool pubkey does not match lp pool pubkey", + )?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let in_oracle_id = in_spot_market.oracle_id(); + let out_oracle_id = out_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let in_oracle = in_oracle.clone(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let in_target_weight = constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, + )?; + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, + )?; + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + &in_oracle, + &out_oracle, + &in_constituent, + &out_constituent, + &in_spot_market, + &out_spot_market, + in_target_weight, + out_target_weight, + in_amount as u128, + constituent_correlations.get_correlation( + in_constituent.constituent_index, + out_constituent.constituent_index, + )?, + )?; + msg!( + "in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}", + in_amount, + out_amount, + in_fee, + out_fee + ); + let out_amount_net_fees = if out_fee > 0 { + out_amount.safe_sub(out_fee.unsigned_abs())? + } else { + out_amount.safe_add(out_fee.unsigned_abs())? + }; + + validate!( + out_amount_net_fees.cast::()? >= min_out_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: out_amount_net_fees({}) < min_out_amount({})", + out_amount_net_fees, min_out_amount + ) + .as_str() + )?; + + validate!( + out_amount_net_fees.cast::()? <= out_constituent.vault_token_balance, + ErrorCode::InsufficientConstituentTokenBalance, + format!( + "Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})", + out_amount_net_fees, out_constituent.vault_token_balance + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee)?; + out_constituent.record_swap_fees(out_fee)?; + + let in_swap_id = get_then_update_id!(in_constituent, next_swap_id); + let out_swap_id = get_then_update_id!(out_constituent, next_swap_id); + + emit_stack::<_, { LPSwapRecord::SIZE }>(LPSwapRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + out_amount: out_amount_net_fees, + in_amount, + out_fee, + in_fee, + out_spot_market_index: out_market_index, + in_spot_market_index: in_market_index, + out_constituent_index: out_constituent.constituent_index, + in_constituent_index: in_constituent.constituent_index, + out_oracle_price: out_oracle.price, + in_oracle_price: in_oracle.price, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + out_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + out_market_target_weight: out_target_weight, + in_swap_id, + out_swap_id, + })?; + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + Some(remaining_accounts), + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.constituent_out_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &out_constituent.lp_pool, + &out_constituent.spot_market_index, + &out_constituent.vault_bump, + ), + out_amount_net_fees.cast::()?, + &Some((*ctx.accounts.out_market_mint).clone()), + Some(remaining_accounts), + )?; + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.constituent_out_token_account.reload()?; + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + Ok(()) +} + +pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolSwapFees<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + in_target_weight: i64, + out_target_weight: i64, +) -> Result<()> { + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + + let lp_pool = &ctx.accounts.lp_pool.load()?; + let in_constituent = ctx.accounts.in_constituent.load()?; + let out_constituent = ctx.accounts.out_constituent.load()?; + let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = + ctx.accounts.constituent_correlations.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + let out_oracle_id = out_spot_market.oracle_id(); + + let (in_oracle, _) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let in_oracle = in_oracle.clone(); + + let (out_oracle, _) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + &in_oracle, + &out_oracle, + &in_constituent, + &out_constituent, + &in_spot_market, + &out_spot_market, + in_target_weight, + out_target_weight, + in_amount as u128, + constituent_correlations.get_correlation( + in_constituent.constituent_index, + out_constituent.constituent_index, + )?, + )?; + msg!( + "in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}", + in_amount, + out_amount, + in_fee, + out_fee + ); + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + + validate!( + state.allow_mint_redeem_lp_pool(), + ErrorCode::MintRedeemLpPoolDisabled, + "Mint/redeem LP pool is disabled" + )?; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Deposit)?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + let lp_price_before = lp_pool.get_price(lp_pool.token_supply)?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![in_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let whitelist_mint = &lp_pool.whitelist_mint; + if !whitelist_mint.eq(&Pubkey::default()) { + validate_whitelist_token( + get_whitelist_token(remaining_accounts)?, + whitelist_mint, + &ctx.accounts.authority.key(), + )?; + } + + let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; + + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let in_oracle_id = in_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let in_oracle = in_oracle.clone(); + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + // TODO: check self.aum validity + + update_spot_market_cumulative_interest(&mut in_spot_market, Some(&in_oracle), now)?; + + msg!("aum: {}", lp_pool.last_aum); + let in_target_weight = if lp_pool.last_aum == 0 { + PERCENTAGE_PRECISION_I64 // 100% weight if no aum + } else { + constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, // TODO: add in_amount * in_oracle to est post add_liquidity aum + )? + }; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + &in_spot_market, + &in_constituent, + in_amount, + &in_oracle, + in_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_amount: {}, in_amount: {}, lp_fee_amount: {}, in_fee_amount: {}", + lp_amount, + in_amount, + lp_fee_amount, + in_fee_amount + ); + + let lp_mint_amount_net_fees = if lp_fee_amount > 0 { + lp_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + validate!( + lp_mint_amount_net_fees >= min_mint_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: lp_mint_amount_net_fees({}) < min_mint_amount({})", + lp_mint_amount_net_fees, min_mint_amount + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + let lp_name = lp_pool.name; + let lp_bump = lp_pool.bump; + + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_name, &lp_bump); + + drop(lp_pool); + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + Some(remaining_accounts), + )?; + + mint_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_amount, + &ctx.accounts.lp_mint, + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_mint_amount_net_fees, + &Some((*ctx.accounts.lp_mint).clone()), + Some(remaining_accounts), + )?; + + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.last_aum = lp_pool.last_aum.safe_add( + in_amount + .cast::()? + .safe_mul(in_oracle.price.cast::()?)? + .safe_div(10_u128.pow(in_spot_market.decimals))?, + )?; + + if lp_pool.last_aum > lp_pool.max_aum { + return Err(ErrorCode::MaxDlpAumBreached.into()); + } + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + + ctx.accounts.lp_mint.reload()?; + let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; + if lp_price_before != 0 { + let price_diff_percent = lp_price_after + .abs_diff(lp_price_before) + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(lp_price_before)?; + + validate!( + price_diff_percent <= PERCENTAGE_PRECISION / 5, + ErrorCode::LpInvariantFailed, + "Removing liquidity resulted in DLP token difference of > 5%" + )?; + } + + let mint_redeem_id = get_then_update_id!(lp_pool, mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 1, + amount: in_amount, + fee: in_fee_amount, + spot_market_index: in_market_index, + constituent_index: in_constituent.constituent_index, + oracle_price: in_oracle.price, + mint: in_constituent.mint, + lp_amount, + lp_fee: lp_fee_amount, + lp_price: lp_price_after, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + })?; + + Ok(()) +} + +pub fn handle_view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolAddLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u128, +) -> Result<()> { + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + let lp_pool = ctx.accounts.lp_pool.load()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let in_constituent = ctx.accounts.in_constituent.load()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let in_oracle = in_oracle.clone(); + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + msg!("aum: {}", lp_pool.last_aum); + let in_target_weight = if lp_pool.last_aum == 0 { + PERCENTAGE_PRECISION_I64 // 100% weight if no aum + } else { + constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, // TODO: add in_amount * in_oracle to est post add_liquidity aum + )? + }; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + &in_spot_market, + &in_constituent, + in_amount, + &in_oracle, + in_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_amount: {}, in_amount: {}, lp_fee_amount: {}, in_fee_amount: {}", + lp_amount, + in_amount, + lp_fee_amount, + in_fee_amount + ); + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + out_market_index: u16, + lp_to_burn: u64, + min_amount_out: u128, +) -> Result<()> { + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + + validate!( + state.allow_mint_redeem_lp_pool(), + ErrorCode::MintRedeemLpPoolDisabled, + "Mint/redeem LP pool is disabled" + )?; + + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + let lp_price_before = lp_pool.get_price(lp_pool.token_supply)?; + + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Withdraw)?; + + // Verify previous settle + let amm_cache: AccountZeroCopy<'_, CacheInfo, _> = ctx.accounts.amm_cache.load_zc()?; + for (i, _) in amm_cache.iter().enumerate() { + let cache_info = amm_cache.get(i as u32); + if cache_info.last_fee_pool_token_amount != 0 && cache_info.last_settle_slot != slot { + msg!( + "Market {} has not been settled in current slot. Last slot: {}", + i, + cache_info.last_settle_slot + ); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![out_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let mut out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let out_oracle_id = out_spot_market.oracle_id(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let out_oracle = out_oracle.clone(); + + // TODO: check self.aum validity + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + update_spot_market_cumulative_interest(&mut out_spot_market, Some(&out_oracle), now)?; + + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, // TODO: remove out_amount * out_oracle to est post remove_liquidity aum + )?; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + &out_spot_market, + &out_constituent, + lp_to_burn, + &out_oracle, + out_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_burn_amount: {}, out_amount: {}, lp_fee_amount: {}, out_fee_amount: {}", + lp_burn_amount, + out_amount, + lp_fee_amount, + out_fee_amount + ); + + let lp_burn_amount_net_fees = if lp_fee_amount > 0 { + lp_burn_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_burn_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + let out_amount_net_fees = if out_fee_amount > 0 { + out_amount.safe_sub(out_fee_amount.unsigned_abs())? + } else { + out_amount.safe_add(out_fee_amount.unsigned_abs())? + }; + + validate!( + out_amount_net_fees >= min_amount_out, + ErrorCode::SlippageOutsideLimit, + "Slippage outside limit: out_amount_net_fees({}) < min_amount_out({})", + out_amount_net_fees, + min_amount_out + )?; + + if out_amount_net_fees > out_constituent.vault_token_balance.cast()? { + let transfer_amount = out_amount_net_fees + .cast::()? + .safe_sub(out_constituent.vault_token_balance)?; + msg!( + "transfering from program vault to constituent vault: {}", + transfer_amount + ); + transfer_from_program_vault( + transfer_amount, + &mut out_spot_market, + &mut out_constituent, + out_oracle.price, + &ctx.accounts.state, + &mut ctx.accounts.spot_market_token_account, + &mut ctx.accounts.constituent_out_token_account, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + &None, + Some(remaining_accounts), + )?; + } + + validate!( + out_amount_net_fees <= out_constituent.vault_token_balance.cast()?, + ErrorCode::InsufficientConstituentTokenBalance, + "Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})", + out_amount_net_fees, + out_constituent.vault_token_balance + )?; + + out_constituent.record_swap_fees(out_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + let lp_name = lp_pool.name; + let lp_bump = lp_pool.bump; + + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_name, &lp_bump); + + drop(lp_pool); + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.authority, + lp_burn_amount, + &None, + Some(remaining_accounts), + )?; + + burn_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_burn_amount_net_fees, + &ctx.accounts.lp_mint, + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.constituent_out_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &out_constituent.lp_pool, + &out_constituent.spot_market_index, + &out_constituent.vault_bump, + ), + out_amount_net_fees.cast::()?, + &None, + Some(remaining_accounts), + )?; + + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.last_aum = lp_pool.last_aum.safe_sub( + out_amount_net_fees + .cast::()? + .safe_mul(out_oracle.price.cast::()?)? + .safe_div(10_u128.pow(out_spot_market.decimals))?, + )?; + + ctx.accounts.constituent_out_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + ctx.accounts.lp_mint.reload()?; + let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; + + if lp_price_after != 0 { + let price_diff_percent = lp_price_after + .abs_diff(lp_price_before) + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(lp_price_before)?; + validate!( + price_diff_percent <= PERCENTAGE_PRECISION / 5, + ErrorCode::LpInvariantFailed, + "Removing liquidity resulted in DLP token difference of > 5%" + )?; + } + + let mint_redeem_id = get_then_update_id!(lp_pool, mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 0, + amount: out_amount, + fee: out_fee_amount, + spot_market_index: out_market_index, + constituent_index: out_constituent.constituent_index, + oracle_price: out_oracle.price, + mint: out_constituent.mint, + lp_amount: lp_burn_amount, + lp_fee: lp_fee_amount, + lp_price: lp_price_after, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: out_target_weight, + })?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolRemoveLiquidityFees<'info>>, + out_market_index: u16, + lp_to_burn: u64, +) -> Result<()> { + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + let lp_pool = ctx.accounts.lp_pool.load()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![out_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + + let out_oracle_id = out_spot_market.oracle_id(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let out_oracle = out_oracle.clone(); + + // TODO: check self.aum validity + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, // TODO: remove out_amount * out_oracle to est post remove_liquidity aum + )?; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + &out_spot_market, + &out_constituent, + lp_to_burn, + &out_oracle, + out_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_burn_amount: {}, out_amount: {}, lp_fee_amount: {}, out_fee_amount: {}", + lp_burn_amount, + out_amount, + lp_fee_amount, + out_fee_amount + ); + + Ok(()) +} + +pub fn handle_update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, +) -> Result<()> { + let clock = Clock::get()?; + let mut constituent = ctx.accounts.constituent.load_mut()?; + let spot_market = ctx.accounts.spot_market.load()?; + + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + + Ok(()) +} + +pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + let spot_market_vault = &ctx.accounts.spot_market_vault; + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + let deposit_plus_token_amount_before = amount.safe_add(spot_market_vault.amount)?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_data), + clock.unix_timestamp, + )?; + + let mut constituent = ctx.accounts.constituent.load_mut()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + let balance_before = constituent.get_full_token_amount(&spot_market)?; + + controller::token::send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_token_account, + &spot_market_vault, + &ctx.accounts.constituent_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &constituent.lp_pool, + &constituent.spot_market_index, + &constituent.vault_bump, + ), + amount, + &Some(*ctx.accounts.mint.clone()), + Some(remaining_accounts), + )?; + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + spot_position, + false, + )?; + + safe_increment!(spot_position.cumulative_deposits, amount.cast()?); + + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.constituent_token_account.reload()?; + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + spot_market.validate_max_token_deposits_and_borrows(false)?; + + validate!( + ctx.accounts.spot_market_vault.amount == deposit_plus_token_amount_before, + ErrorCode::LpInvariantFailed, + "Spot market vault amount mismatch after deposit" + )?; + + let balance_after = constituent.get_full_token_amount(&spot_market)?; + let balance_diff_notional = if spot_market.decimals > 6 { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(oracle_data.price)? + .safe_div(PRICE_PRECISION_I64)? + .safe_div(10_i64.pow(spot_market.decimals - 6))? + } else { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(10_i64.pow(6 - spot_market.decimals))? + .safe_mul(oracle_data.price)? + .safe_div(PRICE_PRECISION_I64)? + }; + + msg!("Balance difference (notional): {}", balance_diff_notional); + + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 100, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault" + )?; + + Ok(()) +} + +pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_data), + clock.unix_timestamp, + )?; + + let mut constituent = ctx.accounts.constituent.load_mut()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + + let mint = &Some(*ctx.accounts.mint.clone()); + transfer_from_program_vault( + amount, + &mut spot_market, + &mut constituent, + oracle_data.price, + &state, + &mut ctx.accounts.spot_market_vault, + &mut ctx.accounts.constituent_token_account, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + mint, + Some(remaining_accounts), + )?; + + Ok(()) +} + +fn transfer_from_program_vault<'info>( + amount: u64, + spot_market: &mut SpotMarket, + constituent: &mut Constituent, + oracle_price: i64, + state: &State, + spot_market_vault: &mut InterfaceAccount<'info, TokenAccount>, + constituent_token_account: &mut InterfaceAccount<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + drift_signer: &AccountInfo<'info>, + mint: &Option>, + remaining_accounts: Option<&mut Peekable>>>, +) -> Result<()> { + constituent.sync_token_balance(constituent_token_account.amount); + + let balance_before = constituent.get_full_token_amount(&spot_market)?; + + let max_transfer = constituent.get_max_transfer(&spot_market)?; + + validate!( + max_transfer >= amount, + ErrorCode::LpInvariantFailed, + "Max transfer ({} is less than amount ({})", + max_transfer, + amount + )?; + + // Execute transfer and sync new balance in the constituent account + controller::token::send_from_program_vault( + &token_program, + &spot_market_vault, + &constituent_token_account, + &drift_signer, + state.signer_nonce, + amount, + mint, + remaining_accounts, + )?; + constituent_token_account.reload()?; + constituent.sync_token_balance(constituent_token_account.amount); + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + spot_position, + true, + )?; + + safe_decrement!(spot_position.cumulative_deposits, amount.cast()?); + + // Re-check spot market invariants + spot_market_vault.reload()?; + spot_market.validate_max_token_deposits_and_borrows(true)?; + math::spot_withdraw::validate_spot_market_vault_amount(&spot_market, spot_market_vault.amount)?; + + // Verify withdraw fully accounted for in BLPosition + let balance_after = constituent.get_full_token_amount(&spot_market)?; + + let balance_diff_notional = if spot_market.decimals > 6 { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(oracle_price)? + .safe_div(PRICE_PRECISION_I64)? + .safe_div(10_i64.pow(spot_market.decimals - 6))? + } else { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(10_i64.pow(6 - spot_market.decimals))? + .safe_mul(oracle_price)? + .safe_div(PRICE_PRECISION_I64)? + }; + + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 100, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault" + )?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct DepositProgramVault<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + constraint = &constituent.load()?.mint.eq(&constituent_token_account.mint), + )] + pub constituent_token_account: Box>, + #[account( + mut, + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + address = spot_market.load()?.vault, + )] + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = spot_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct WithdrawProgramVault<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + constraint = &constituent.load()?.mint.eq(&constituent_token_account.mint), + )] + pub constituent_token_account: Box>, + #[account( + mut, + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + address = spot_market.load()?.vault, + token::authority = drift_signer, + )] + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = spot_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentOracleInfo<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentTargetBase<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmConstituentMappingZeroCopy checks + pub amm_constituent_mapping: AccountInfo<'info>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + pub amm_cache: AccountInfo<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, +} + +#[derive(Accounts)] +pub struct UpdateLPPoolAum<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, +} + +/// `in`/`out` is in the program's POV for this swap. So `user_in_token_account` is the user owned token account +/// for the `in` token for this swap. +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPPoolSwap<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and in ix + pub constituent_target_base: AccountInfo<'info>, + + /// CHECK: checked in ConstituentCorrelationsZeroCopy checks and in ix + pub constituent_correlations: AccountInfo<'info>, + + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + )] + pub user_in_token_account: Box>, + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + )] + pub user_out_token_account: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump=in_constituent.load()?.bump, + constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump=out_constituent.load()?.bump, + constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = in_market_mint.key() == in_constituent.load()?.mint, + )] + pub in_market_mint: Box>, + #[account( + constraint = out_market_mint.key() == out_constituent.load()?.mint, + )] + pub out_market_mint: Box>, + + pub authority: Signer<'info>, + + // TODO: in/out token program + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct ViewLPPoolSwapFees<'info> { + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and in ix + pub constituent_target_base: AccountInfo<'info>, + + /// CHECK: checked in ConstituentCorrelationsZeroCopy checks and in ix + pub constituent_correlations: AccountInfo<'info>, + + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump=in_constituent.load()?.bump, + constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump=out_constituent.load()?.bump, + constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + pub authority: Signer<'info>, + + // TODO: in/out token program + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct LPPoolAddLiquidity<'info> { + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub in_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + constraint = + in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + )] + pub user_in_token_account: Box>, + + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub constituent_in_token_account: Box>, + + #[account( + mut, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct ViewLPPoolAddLiquidityFees<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub in_market_mint: Box>, + #[account( + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub in_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction( + out_market_index: u16, +)] +pub struct LPPoolRemoveLiquidity<'info> { + pub state: Box>, + #[account( + constraint = drift_signer.key() == state.signer + )] + /// CHECK: drift_signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub out_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + constraint = + out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + )] + pub user_out_token_account: Box>, + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, + + #[account( + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump, + )] + /// CHECK: checked in AmmCacheZeroCopy checks + pub amm_cache: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct ViewLPPoolRemoveLiquidityFees<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub out_market_mint: Box>, + #[account( + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, +} diff --git a/programs/drift/src/instructions/mod.rs b/programs/drift/src/instructions/mod.rs index 0caa84f731..d2267d7d5a 100644 --- a/programs/drift/src/instructions/mod.rs +++ b/programs/drift/src/instructions/mod.rs @@ -2,6 +2,8 @@ pub use admin::*; pub use constraints::*; pub use if_staker::*; pub use keeper::*; +pub use lp_admin::*; +pub use lp_pool::*; pub use pyth_lazer_oracle::*; pub use pyth_pull_oracle::*; pub use user::*; @@ -10,6 +12,8 @@ mod admin; mod constraints; mod if_staker; mod keeper; +mod lp_admin; +mod lp_pool; pub mod optional_accounts; mod pyth_lazer_oracle; mod pyth_pull_oracle; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 43653c8c1a..4e71caf145 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1006,6 +1006,18 @@ pub mod drift { ) } + pub fn initialize_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeAmmCache<'info>>, + ) -> Result<()> { + handle_initialize_amm_cache(ctx) + } + + pub fn update_initial_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + ) -> Result<()> { + handle_update_initial_amm_cache_info(ctx) + } + pub fn initialize_prediction_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AdminUpdatePerpMarket<'info>>, ) -> Result<()> { @@ -1057,6 +1069,32 @@ pub mod drift { handle_update_perp_market_expiry(ctx, expiry_ts) } + pub fn update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_paused_operations(ctx, lp_paused_operations) + } + + pub fn update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_status(ctx, lp_status) + } + + pub fn update_perp_market_lp_pool_fee_transfer_scalar( + ctx: Context, + optional_lp_fee_transfer_scalar: Option, + optional_lp_net_pnl_transfer_scalar: Option, + ) -> Result<()> { + handle_update_perp_market_lp_pool_fee_transfer_scalar( + ctx, + optional_lp_fee_transfer_scalar, + optional_lp_net_pnl_transfer_scalar, + ) + } + pub fn settle_expired_market_pools_to_revenue_pool( ctx: Context, ) -> Result<()> { @@ -1340,7 +1378,7 @@ pub mod drift { } pub fn update_perp_market_contract_tier( - ctx: Context, + ctx: Context, contract_tier: ContractTier, ) -> Result<()> { handle_update_perp_market_contract_tier(ctx, contract_tier) @@ -1736,6 +1774,31 @@ pub mod drift { handle_initialize_high_leverage_mode_config(ctx, max_users) } + pub fn initialize_lp_pool( + ctx: Context, + name: [u8; 32], + min_mint_fee: i64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + whitelist_mint: Pubkey, + ) -> Result<()> { + handle_initialize_lp_pool( + ctx, + name, + min_mint_fee, + max_aum, + max_settle_quote_amount_per_market, + whitelist_mint, + ) + } + + pub fn increase_lp_pool_max_aum( + ctx: Context, + new_max_aum: u128, + ) -> Result<()> { + handle_increase_lp_pool_max_aum(ctx, new_max_aum) + } + pub fn update_high_leverage_mode_config( ctx: Context, max_users: u32, @@ -1800,6 +1863,263 @@ pub mod drift { ) -> Result<()> { handle_update_feature_bit_flags_median_trigger_price(ctx, enable) } + + pub fn update_feature_bit_flags_settle_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_settle_lp_pool(ctx, enable) + } + + pub fn update_feature_bit_flags_swap_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_swap_lp_pool(ctx, enable) + } + + pub fn update_feature_bit_flags_mint_redeem_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_mint_redeem_lp_pool(ctx, enable) + } + + pub fn initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, + ) -> Result<()> { + handle_initialize_constituent( + ctx, + spot_market_index, + decimals, + max_weight_deviation, + swap_fee_min, + swap_fee_max, + max_borrow_token_amount, + oracle_staleness_threshold, + cost_to_trade, + constituent_derivative_index, + constituent_derivative_depeg_threshold, + derivative_weight, + volatility, + gamma_execution, + gamma_inventory, + xi, + new_constituent_correlations, + ) + } + + pub fn update_constituent_status<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentStatus<'info>>, + new_status: u8, + ) -> Result<()> { + handle_update_constituent_status(ctx, new_status) + } + + pub fn update_constituent_paused_operations<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentPausedOperations<'info>>, + paused_operations: u8, + ) -> Result<()> { + handle_update_constituent_paused_operations(ctx, paused_operations) + } + + pub fn update_constituent_params( + ctx: Context, + constituent_params: ConstituentParams, + ) -> Result<()> { + handle_update_constituent_params(ctx, constituent_params) + } + + pub fn update_lp_pool_params( + ctx: Context, + lp_pool_params: LpPoolParams, + ) -> Result<()> { + handle_update_lp_pool_params(ctx, lp_pool_params) + } + + pub fn add_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_add_amm_constituent_data(ctx, amm_constituent_mapping_data) + } + + pub fn update_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_update_amm_constituent_mapping_data(ctx, amm_constituent_mapping_data) + } + + pub fn remove_amm_constituent_mapping_data<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RemoveAmmConstituentMappingData<'info>>, + perp_market_index: u16, + constituent_index: u16, + ) -> Result<()> { + handle_remove_amm_constituent_mapping_data(ctx, perp_market_index, constituent_index) + } + + pub fn update_constituent_correlation_data( + ctx: Context, + index1: u16, + index2: u16, + correlation: i64, + ) -> Result<()> { + handle_update_constituent_correlation_data(ctx, index1, index2, correlation) + } + + pub fn update_lp_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, + ) -> Result<()> { + handle_update_constituent_target_base(ctx) + } + + pub fn update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, + ) -> Result<()> { + handle_update_lp_pool_aum(ctx) + } + + pub fn update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, + ) -> Result<()> { + handle_update_amm_cache(ctx) + } + + pub fn override_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + market_index: u16, + override_params: OverrideAmmCacheParams, + ) -> Result<()> { + handle_override_amm_cache_info(ctx, market_index, override_params) + } + + pub fn lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, + ) -> Result<()> { + handle_lp_pool_swap( + ctx, + in_market_index, + out_market_index, + in_amount, + min_out_amount, + ) + } + + pub fn view_lp_pool_swap_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolSwapFees<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + in_target_weight: i64, + out_target_weight: i64, + ) -> Result<()> { + handle_view_lp_pool_swap_fees( + ctx, + in_market_index, + out_market_index, + in_amount, + in_target_weight, + out_target_weight, + ) + } + + pub fn lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, + ) -> Result<()> { + handle_lp_pool_add_liquidity(ctx, in_market_index, in_amount, min_mint_amount) + } + + pub fn lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + in_market_index: u16, + in_amount: u64, + min_out_amount: u128, + ) -> Result<()> { + handle_lp_pool_remove_liquidity(ctx, in_market_index, in_amount, min_out_amount) + } + + pub fn view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolAddLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u128, + ) -> Result<()> { + handle_view_lp_pool_add_liquidity_fees(ctx, in_market_index, in_amount) + } + + pub fn view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolRemoveLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u64, + ) -> Result<()> { + handle_view_lp_pool_remove_liquidity_fees(ctx, in_market_index, in_amount) + } + + pub fn begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, + ) -> Result<()> { + handle_begin_lp_swap(ctx, in_market_index, out_market_index, amount_in) + } + + pub fn end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + ) -> Result<()> { + handle_end_lp_swap(ctx) + } + + pub fn update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, + ) -> Result<()> { + handle_update_constituent_oracle_info(ctx) + } + + pub fn deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_deposit_to_program_vault(ctx, amount) + } + + pub fn withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_withdraw_from_program_vault(ctx, amount) + } + + pub fn settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, + ) -> Result<()> { + handle_settle_perp_to_lp_pool(ctx) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index 1c127315ae..e5bf55798d 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -53,6 +53,8 @@ pub const PERCENTAGE_PRECISION: u128 = 1_000_000; // expo -6 (represents 100%) pub const PERCENTAGE_PRECISION_I128: i128 = PERCENTAGE_PRECISION as i128; pub const PERCENTAGE_PRECISION_U64: u64 = PERCENTAGE_PRECISION as u64; pub const PERCENTAGE_PRECISION_I64: i64 = PERCENTAGE_PRECISION as i64; +pub const PERCENTAGE_PRECISION_I32: i32 = PERCENTAGE_PRECISION as i32; + pub const TEN_BPS: i128 = PERCENTAGE_PRECISION_I128 / 1000; pub const TEN_BPS_I64: i64 = TEN_BPS as i64; pub const TWO_PT_TWO_PCT: i128 = 22_000; diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs new file mode 100644 index 0000000000..145442e395 --- /dev/null +++ b/programs/drift/src/math/lp_pool.rs @@ -0,0 +1,217 @@ +pub mod perp_lp_pool_settlement { + use core::slice::Iter; + use std::iter::Peekable; + + use crate::error::ErrorCode; + use crate::math::casting::Cast; + use crate::state::spot_market::SpotBalanceType; + use crate::{ + math::safe_math::SafeMath, + state::{amm_cache::CacheInfo, perp_market::PerpMarket, spot_market::SpotMarket}, + *, + }; + use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + + #[derive(Debug, Clone, Copy)] + pub struct SettlementResult { + pub amount_transferred: u64, + pub direction: SettlementDirection, + pub fee_pool_used: u128, + pub pnl_pool_used: u128, + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub enum SettlementDirection { + ToLpPool, + FromLpPool, + None, + } + + pub struct SettlementContext<'a> { + pub quote_owed_from_lp: i64, + pub quote_constituent_token_balance: u64, + pub fee_pool_balance: u128, + pub pnl_pool_balance: u128, + pub quote_market: &'a SpotMarket, + pub max_settle_quote_amount: u64, + } + + pub fn calculate_settlement_amount(ctx: &SettlementContext) -> Result { + if ctx.quote_owed_from_lp > 0 { + calculate_lp_to_perp_settlement(ctx) + } else if ctx.quote_owed_from_lp < 0 { + calculate_perp_to_lp_settlement(ctx) + } else { + Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + } + + pub fn validate_settlement_amount( + ctx: &SettlementContext, + result: &SettlementResult, + ) -> Result<()> { + if result.amount_transferred > ctx.max_settle_quote_amount as u64 { + msg!( + "Amount to settle exceeds maximum allowed, {} > {}", + result.amount_transferred, + ctx.max_settle_quote_amount + ); + return Err(ErrorCode::LpPoolSettleInvariantBreached.into()); + } + Ok(()) + } + + fn calculate_lp_to_perp_settlement(ctx: &SettlementContext) -> Result { + if ctx.quote_constituent_token_balance == 0 { + return Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }); + } + + let amount_to_send = ctx + .quote_owed_from_lp + .cast::()? + .min(ctx.quote_constituent_token_balance) + .min(ctx.max_settle_quote_amount); + + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + + fn calculate_perp_to_lp_settlement(ctx: &SettlementContext) -> Result { + let amount_to_send = (ctx.quote_owed_from_lp.abs() as u64).min(ctx.max_settle_quote_amount); + + if ctx.fee_pool_balance >= amount_to_send as u128 { + // Fee pool can cover entire amount + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::ToLpPool, + fee_pool_used: amount_to_send as u128, + pnl_pool_used: 0, + }) + } else { + // Need to use both fee pool and pnl pool + let remaining_amount = (amount_to_send as u128).safe_sub(ctx.fee_pool_balance)?; + let pnl_pool_used = remaining_amount.min(ctx.pnl_pool_balance); + let actual_transfer = ctx.fee_pool_balance.safe_add(pnl_pool_used)?; + + Ok(SettlementResult { + amount_transferred: actual_transfer as u64, + direction: SettlementDirection::ToLpPool, + fee_pool_used: ctx.fee_pool_balance, + pnl_pool_used, + }) + } + } + + pub fn execute_token_transfer<'info>( + token_program: &Interface<'info, TokenInterface>, + from_vault: &InterfaceAccount<'info, TokenAccount>, + to_vault: &InterfaceAccount<'info, TokenAccount>, + signer: &AccountInfo<'info>, + signer_seed: &[&[u8]], + amount: u64, + remaining_accounts: Option<&mut Peekable>>>, + ) -> Result<()> { + controller::token::send_from_program_vault_with_signature_seeds( + token_program, + from_vault, + to_vault, + signer, + signer_seed, + amount, + &None, + remaining_accounts, + ) + } + + // Market state updates + pub fn update_perp_market_pools_and_quote_market_balance( + perp_market: &mut PerpMarket, + result: &SettlementResult, + quote_spot_market: &mut SpotMarket, + ) -> Result<()> { + match result.direction { + SettlementDirection::FromLpPool => { + controller::spot_balance::update_spot_balances( + result.amount_transferred as u128, + &SpotBalanceType::Deposit, + quote_spot_market, + &mut perp_market.amm.fee_pool, + false, + )?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + controller::spot_balance::update_spot_balances( + result.fee_pool_used, + &SpotBalanceType::Borrow, + quote_spot_market, + &mut perp_market.amm.fee_pool, + true, + )?; + } + if result.pnl_pool_used > 0 { + controller::spot_balance::update_spot_balances( + result.pnl_pool_used, + &SpotBalanceType::Borrow, + quote_spot_market, + &mut perp_market.pnl_pool, + true, + )?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } + + pub fn update_cache_info( + cache_info: &mut CacheInfo, + result: &SettlementResult, + new_quote_owed: i64, + slot: u64, + now: i64, + ) -> Result<()> { + cache_info.quote_owed_from_lp_pool = new_quote_owed; + cache_info.last_settle_amount = result.amount_transferred; + cache_info.last_settle_slot = slot; + cache_info.last_settle_ts = now; + cache_info.last_settle_amm_ex_fees = cache_info.last_exchange_fees; + cache_info.last_settle_amm_pnl = cache_info.last_net_pnl_pool_token_amount; + + match result.direction { + SettlementDirection::FromLpPool => { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_add(result.amount_transferred as u128)?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_sub(result.fee_pool_used)?; + } + if result.pnl_pool_used > 0 { + cache_info.last_net_pnl_pool_token_amount = cache_info + .last_net_pnl_pool_token_amount + .safe_sub(result.pnl_pool_used as i128)?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } +} diff --git a/programs/drift/src/math/mod.rs b/programs/drift/src/math/mod.rs index 89edbdafc5..17a972d826 100644 --- a/programs/drift/src/math/mod.rs +++ b/programs/drift/src/math/mod.rs @@ -16,6 +16,7 @@ pub mod funding; pub mod helpers; pub mod insurance; pub mod liquidation; +pub mod lp_pool; pub mod margin; pub mod matching; pub mod oracle; diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index 78be9ce782..d569faf718 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -11,6 +11,7 @@ use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::PerpMarket; use crate::state::state::{OracleGuardRails, ValidityGuardRails}; use crate::state::user::MarketType; +use std::convert::TryFrom; use std::fmt; #[cfg(test)] @@ -57,6 +58,37 @@ impl fmt::Display for OracleValidity { } } +impl TryFrom for OracleValidity { + type Error = ErrorCode; + + fn try_from(v: u8) -> DriftResult { + match v { + 0 => Ok(OracleValidity::NonPositive), + 1 => Ok(OracleValidity::TooVolatile), + 2 => Ok(OracleValidity::TooUncertain), + 3 => Ok(OracleValidity::StaleForMargin), + 4 => Ok(OracleValidity::InsufficientDataPoints), + 5 => Ok(OracleValidity::StaleForAMM), + 6 => Ok(OracleValidity::Valid), + _ => panic!("Invalid OracleValidity"), + } + } +} + +impl From for u8 { + fn from(src: OracleValidity) -> u8 { + match src { + OracleValidity::NonPositive => 0, + OracleValidity::TooVolatile => 1, + OracleValidity::TooUncertain => 2, + OracleValidity::StaleForMargin => 3, + OracleValidity::InsufficientDataPoints => 4, + OracleValidity::StaleForAMM => 5, + OracleValidity::Valid => 6, + } + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum DriftAction { UpdateFunding, @@ -70,6 +102,9 @@ pub enum DriftAction { UpdateAMMCurve, OracleOrderPrice, UseMMOraclePrice, + UpdateLpConstituentTargetBase, + UpdateLpPoolAum, + LpPoolSwap, } pub fn is_oracle_valid_for_action( @@ -117,7 +152,10 @@ pub fn is_oracle_valid_for_action( | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ), - DriftAction::FillOrderMatch => !matches!( + DriftAction::FillOrderMatch + | DriftAction::UpdateLpConstituentTargetBase + | DriftAction::UpdateLpPoolAum + | DriftAction::LpPoolSwap => !matches!( oracle_validity, OracleValidity::NonPositive | OracleValidity::TooVolatile diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs new file mode 100644 index 0000000000..f17e88cc96 --- /dev/null +++ b/programs/drift/src/state/amm_cache.rs @@ -0,0 +1,352 @@ +use std::convert::TryFrom; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::amm::calculate_net_user_pnl; +use crate::math::casting::Cast; +use crate::math::oracle::{oracle_validity, LogMode}; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::oracle::MMOraclePriceData; +use crate::state::oracle_map::OracleIdentifier; +use crate::state::perp_market::PerpMarket; +use crate::state::spot_market::{SpotBalance, SpotMarket}; +use crate::state::state::State; +use crate::state::traits::Size; +use crate::state::zero_copy::HasLen; +use crate::state::zero_copy::{AccountZeroCopy, AccountZeroCopyMut}; +use crate::validate; +use crate::OracleSource; +use crate::{impl_zero_copy_loader, OracleGuardRails}; + +use anchor_lang::prelude::*; + +use super::user::MarketType; + +pub const AMM_POSITIONS_CACHE: &str = "amm_cache"; + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmCache { + pub bump: u8, + _padding: [u8; 3], + pub cache: Vec, +} + +#[zero_copy] +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +#[repr(C)] +pub struct CacheInfo { + pub oracle: Pubkey, + pub last_fee_pool_token_amount: u128, + pub last_net_pnl_pool_token_amount: i128, + pub last_exchange_fees: u128, + pub last_settle_amm_ex_fees: u128, + pub last_settle_amm_pnl: i128, + /// BASE PRECISION + pub position: i64, + pub slot: u64, + pub last_settle_amount: u64, + pub last_settle_slot: u64, + pub last_settle_ts: i64, + pub quote_owed_from_lp_pool: i64, + pub oracle_price: i64, + pub oracle_slot: u64, + pub oracle_source: u8, + pub oracle_validity: u8, + pub lp_status_for_perp_market: u8, + pub _padding: [u8; 13], +} + +impl Size for CacheInfo { + const SIZE: usize = 192; +} + +impl Default for CacheInfo { + fn default() -> Self { + CacheInfo { + position: 0i64, + slot: 0u64, + oracle_price: 0i64, + oracle_slot: 0u64, + oracle_validity: 0u8, + oracle: Pubkey::default(), + last_fee_pool_token_amount: 0u128, + last_net_pnl_pool_token_amount: 0i128, + last_exchange_fees: 0u128, + last_settle_amount: 0u64, + last_settle_slot: 0u64, + last_settle_ts: 0i64, + last_settle_amm_pnl: 0i128, + last_settle_amm_ex_fees: 0u128, + oracle_source: 0u8, + quote_owed_from_lp_pool: 0i64, + lp_status_for_perp_market: 0u8, + _padding: [0u8; 13], + } + } +} + +impl CacheInfo { + pub fn get_oracle_source(&self) -> DriftResult { + Ok(OracleSource::try_from(self.oracle_source)?) + } + + pub fn oracle_id(&self) -> DriftResult { + let oracle_source = self.get_oracle_source()?; + Ok((self.oracle, oracle_source)) + } + + pub fn get_last_available_amm_token_amount(&self) -> DriftResult { + let last_available_balance = self + .last_fee_pool_token_amount + .cast::()? + .safe_add(self.last_net_pnl_pool_token_amount)?; + Ok(last_available_balance) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + self.oracle = perp_market.amm.oracle; + self.oracle_source = u8::from(perp_market.amm.oracle_source); + self.position = perp_market + .amm + .get_protocol_owned_position()? + .safe_mul(-1)?; + self.lp_status_for_perp_market = perp_market.lp_status; + Ok(()) + } + + pub fn update_oracle_info( + &mut self, + clock_slot: u64, + oracle_price_data: &MMOraclePriceData, + perp_market: &PerpMarket, + oracle_guard_rails: &OracleGuardRails, + ) -> DriftResult<()> { + let safe_oracle_data = oracle_price_data.get_safe_oracle_price_data(); + self.oracle_price = safe_oracle_data.price; + self.oracle_slot = clock_slot.safe_sub(safe_oracle_data.delay.max(0) as u64)?; + self.slot = clock_slot; + let validity = oracle_validity( + MarketType::Perp, + perp_market.market_index, + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + &safe_oracle_data, + &oracle_guard_rails.validity, + perp_market.get_max_confidence_interval_multiplier()?, + &perp_market.amm.oracle_source, + LogMode::SafeMMOracle, + perp_market.amm.oracle_slot_delay_override, + )?; + self.oracle_validity = u8::from(validity); + Ok(()) + } +} + +#[zero_copy] +#[derive(Default, Debug)] +#[repr(C)] +pub struct AmmCacheFixed { + pub bump: u8, + _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmCacheFixed { + fn len(&self) -> u32 { + self.len + } +} + +impl AmmCache { + pub fn space(num_markets: usize) -> usize { + 8 + 8 + 4 + num_markets * CacheInfo::SIZE + } + + pub fn validate(&self, state: &State) -> DriftResult<()> { + validate!( + self.cache.len() == state.number_of_markets as usize, + ErrorCode::DefaultError, + "Number of amm positions is different than number of markets" + )?; + Ok(()) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + let cache_info = self.cache.get_mut(perp_market.market_index as usize); + if let Some(cache_info) = cache_info { + cache_info.update_perp_market_fields(perp_market)?; + } else { + msg!( + "Updating amm cache from admin with perp market index not found in cache: {}", + perp_market.market_index + ); + return Err(ErrorCode::DefaultError.into()); + } + + Ok(()) + } + + pub fn update_oracle_info( + &mut self, + clock_slot: u64, + market_index: u16, + oracle_price_data: &MMOraclePriceData, + perp_market: &PerpMarket, + oracle_guard_rails: &OracleGuardRails, + ) -> DriftResult<()> { + let cache_info = self.cache.get_mut(market_index as usize); + if let Some(cache_info) = cache_info { + cache_info.update_oracle_info( + clock_slot, + oracle_price_data, + perp_market, + oracle_guard_rails, + )?; + } else { + msg!( + "Updating amm cache from admin with perp market index not found in cache: {}", + market_index + ); + return Err(ErrorCode::DefaultError.into()); + } + + Ok(()) + } +} + +impl_zero_copy_loader!(AmmCache, crate::id, AmmCacheFixed, CacheInfo); + +impl<'a> AccountZeroCopy<'a, CacheInfo, AmmCacheFixed> { + pub fn check_settle_staleness(&self, slot: u64, threshold_slot_diff: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.last_settle_slot < slot.saturating_sub(threshold_slot_diff) { + msg!("AMM settle data is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_perp_market_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.slot < slot.saturating_sub(threshold) { + msg!("Perp market cache info is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_oracle_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.oracle_slot < slot.saturating_sub(threshold) { + msg!( + "Perp market cache info is stale for perp market {}. oracle slot: {}, slot: {}", + i, + cache_info.oracle_slot, + slot + ); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } +} + +impl<'a> AccountZeroCopyMut<'a, CacheInfo, AmmCacheFixed> { + pub fn update_amount_owed_from_lp_pool( + &mut self, + perp_market: &PerpMarket, + quote_market: &SpotMarket, + ) -> DriftResult<()> { + if perp_market.lp_fee_transfer_scalar == 0 + && perp_market.lp_exchange_fee_excluscion_scalar == 0 + { + msg!( + "lp_fee_transfer_scalar and lp_net_pnl_transfer_scalar are 0 for perp market {}. not updating quote amount owed in cache", + perp_market.market_index + ); + return Ok(()); + } + + let cached_info = self.get_mut(perp_market.market_index as u32); + + let fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + "e_market, + perp_market.amm.fee_pool.balance_type(), + )?; + + let net_pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + "e_market, + perp_market.pnl_pool.balance_type(), + )? + .cast::()? + .safe_sub(calculate_net_user_pnl( + &perp_market.amm, + cached_info.oracle_price, + )?)?; + + let amm_amount_available = + net_pnl_pool_token_amount.safe_add(fee_pool_token_amount.cast::()?)?; + + if cached_info.last_net_pnl_pool_token_amount == 0 + && cached_info.last_fee_pool_token_amount == 0 + && cached_info.last_exchange_fees == 0 + { + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + cached_info.last_exchange_fees = perp_market.amm.total_exchange_fee; + cached_info.last_settle_amm_ex_fees = perp_market.amm.total_exchange_fee; + cached_info.last_settle_amm_pnl = net_pnl_pool_token_amount; + return Ok(()); + } + + let exchange_fee_delta = perp_market + .amm + .total_exchange_fee + .saturating_sub(cached_info.last_exchange_fees); + + let amount_to_send_to_lp_pool = amm_amount_available + .safe_sub(cached_info.get_last_available_amm_token_amount()?)? + .safe_mul(perp_market.lp_fee_transfer_scalar as i128)? + .safe_div_ceil(100)? + .safe_sub( + exchange_fee_delta + .cast::()? + .safe_mul(perp_market.lp_exchange_fee_excluscion_scalar as i128)? + .safe_div_ceil(100)?, + )?; + + cached_info.quote_owed_from_lp_pool = cached_info + .quote_owed_from_lp_pool + .safe_sub(amount_to_send_to_lp_pool.cast::()?)?; + + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + cached_info.last_exchange_fees = perp_market.amm.total_exchange_fee; + + Ok(()) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + let cache_info = self.get_mut(perp_market.market_index as u32); + cache_info.update_perp_market_fields(perp_market)?; + + Ok(()) + } +} diff --git a/programs/drift/src/state/constituent_map.rs b/programs/drift/src/state/constituent_map.rs new file mode 100644 index 0000000000..e14bf7f0c0 --- /dev/null +++ b/programs/drift/src/state/constituent_map.rs @@ -0,0 +1,253 @@ +use anchor_lang::accounts::account_loader::AccountLoader; +use std::cell::{Ref, RefMut}; +use std::collections::{BTreeMap, BTreeSet}; +use std::iter::Peekable; +use std::slice::Iter; + +use anchor_lang::prelude::{AccountInfo, Pubkey}; + +use anchor_lang::Discriminator; +use arrayref::array_ref; + +use crate::error::{DriftResult, ErrorCode}; + +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::traits::Size; +use crate::{msg, validate}; +use std::panic::Location; + +use super::lp_pool::Constituent; + +pub struct ConstituentMap<'a>(pub BTreeMap>); + +impl<'a> ConstituentMap<'a> { + #[track_caller] + #[inline(always)] + pub fn get_ref(&self, constituent_index: &u16) -> DriftResult> { + let loader = match self.0.get(constituent_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load() { + Ok(constituent) => Ok(constituent), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_ref_mut(&self, market_index: &u16) -> DriftResult> { + let loader = match self.0.get(market_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load_mut() { + Ok(perp_market) => Ok(perp_market), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + pub fn load<'b, 'c>( + writable_constituents: &'b ConstituentSet, + lp_pool_key: &Pubkey, + account_info_iter: &'c mut Peekable>>, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + while let Some(account_info) = account_info_iter.peek() { + if account_info.owner != &crate::ID { + break; + } + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + msg!( + "didnt match constituent size, {}, {}", + data.len(), + expected_data_len + ); + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + msg!( + "didnt match account discriminator {:?}, {:?}", + account_discriminator, + constituent_discriminator + ); + break; + } + + // Pubkey + let constituent_lp_key = Pubkey::from(*array_ref![data, 72, 32]); + validate!( + &constituent_lp_key == lp_pool_key, + ErrorCode::InvalidConstituent, + "Constituent lp pool pubkey does not match lp pool pubkey" + )?; + + // constituent index 276 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 292, 2]); + if constituent_map.0.contains_key(&constituent_index) { + msg!( + "Can not include same constituent index twice {}", + constituent_index + ); + return Err(ErrorCode::InvalidConstituent); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + let is_writable = account_info.is_writable; + if writable_constituents.contains(&constituent_index) && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } +} + +#[cfg(test)] +impl<'a> ConstituentMap<'a> { + pub fn load_one<'c: 'a>( + account_info: &'c AccountInfo<'a>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + // market index 1160 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 292, 2]); + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = + AccountLoader::try_from(account_info).or(Err(ErrorCode::InvalidMarketAccount))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + + Ok(constituent_map) + } + + pub fn load_multiple<'c: 'a>( + account_info: Vec<&'c AccountInfo<'a>>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let account_info_iter = account_info.into_iter(); + for account_info in account_info_iter { + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let constituent_index = u16::from_le_bytes(*array_ref![data, 292, 2]); + + if constituent_map.0.contains_key(&constituent_index) { + msg!( + "Can not include same constituent index twice {}", + constituent_index + ); + return Err(ErrorCode::InvalidConstituent); + } + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } + + pub fn empty() -> Self { + ConstituentMap(BTreeMap::new()) + } +} + +pub(crate) type ConstituentSet = BTreeSet; diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index f4806fa5be..55e9cecaeb 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -737,3 +737,112 @@ pub fn emit_buffers( Ok(()) } + +#[event] +#[derive(Default)] +pub struct LPSettleRecord { + pub record_id: u64, + // previous settle unix timestamp + pub last_ts: i64, + // previous settle slot + pub last_slot: u64, + // current settle unix timestamp + pub ts: i64, + // current slot + pub slot: u64, + // amm perp market index + pub perp_market_index: u16, + // token amount to settle to lp (positive is from amm to lp, negative lp to amm) + pub settle_to_lp_amount: i64, + // quote pnl of amm since last settle + pub perp_amm_pnl_delta: i64, + // exchange fees earned by market/amm since last settle + pub perp_amm_ex_fee_delta: i64, + // current aum of lp + pub lp_aum: u128, + // current mint price of lp + pub lp_price: u128, +} + +#[event] +#[derive(Default)] +pub struct LPSwapRecord { + pub ts: i64, + pub slot: u64, + pub authority: Pubkey, + /// precision: out market mint precision, gross fees + pub out_amount: u128, + /// precision: in market mint precision, gross fees + pub in_amount: u128, + /// precision: fee on amount_out, in market mint precision + pub out_fee: i128, + /// precision: fee on amount_in, out market mint precision + pub in_fee: i128, + // out spot market index + pub out_spot_market_index: u16, + // in spot market index + pub in_spot_market_index: u16, + // out constituent index + pub out_constituent_index: u16, + // in constituent index + pub in_constituent_index: u16, + /// precision: PRICE_PRECISION + pub out_oracle_price: i64, + /// precision: PRICE_PRECISION + pub in_oracle_price: i64, + /// LPPool last_aum, QUOTE_PRECISION + pub last_aum: u128, + pub last_aum_slot: u64, + /// PERCENTAGE_PRECISION + pub in_market_current_weight: i64, + /// PERCENTAGE_PRECISION + pub out_market_current_weight: i64, + /// PERCENTAGE_PRECISION + pub in_market_target_weight: i64, + /// PERCENTAGE_PRECISION + pub out_market_target_weight: i64, + pub in_swap_id: u64, + pub out_swap_id: u64, +} + +impl Size for LPSwapRecord { + const SIZE: usize = 376; +} + +#[event] +#[derive(Default)] +pub struct LPMintRedeemRecord { + pub ts: i64, + pub slot: u64, + pub authority: Pubkey, + pub description: u8, + /// precision: continutent mint precision, gross fees + pub amount: u128, + /// precision: fee on amount, constituent market mint precision + pub fee: i128, + // spot market index + pub spot_market_index: u16, + // constituent index + pub constituent_index: u16, + /// precision: PRICE_PRECISION + pub oracle_price: i64, + /// token mint + pub mint: Pubkey, + /// lp amount, lp mint precision + pub lp_amount: u64, + /// lp fee, lp mint precision + pub lp_fee: i64, + /// the fair price of the lp token, PRICE_PRECISION + pub lp_price: u128, + pub mint_redeem_id: u64, + /// LPPool last_aum + pub last_aum: u128, + pub last_aum_slot: u64, + /// PERCENTAGE_PRECISION + pub in_market_current_weight: i64, + pub in_market_target_weight: i64, +} + +impl Size for LPMintRedeemRecord { + const SIZE: usize = 328; +} diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs new file mode 100644 index 0000000000..9552036597 --- /dev/null +++ b/programs/drift/src/state/lp_pool.rs @@ -0,0 +1,1630 @@ +use std::collections::BTreeMap; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::constants::{ + BASE_PRECISION_I128, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, +}; +use crate::math::safe_math::SafeMath; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::math::spot_balance::{get_signed_token_amount, get_token_amount}; +use crate::state::amm_cache::{AmmCacheFixed, CacheInfo}; +use crate::state::constituent_map::ConstituentMap; +use crate::state::paused_operations::ConstituentLpOperation; +use crate::state::spot_market_map::SpotMarketMap; +use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use enumflags2::BitFlags; + +use super::oracle::OraclePriceData; +use super::spot_market::SpotMarket; +use super::zero_copy::{AccountZeroCopy, AccountZeroCopyMut, HasLen}; +use crate::state::spot_market::{SpotBalance, SpotBalanceType}; +use crate::state::traits::Size; +use crate::{impl_zero_copy_loader, validate}; + +pub const LP_POOL_PDA_SEED: &str = "lp_pool"; +pub const AMM_MAP_PDA_SEED: &str = "AMM_MAP"; +pub const CONSTITUENT_PDA_SEED: &str = "CONSTITUENT"; +pub const CONSTITUENT_TARGET_BASE_PDA_SEED: &str = "constituent_target_base"; +pub const CONSTITUENT_CORRELATIONS_PDA_SEED: &str = "constituent_correlations"; +pub const CONSTITUENT_VAULT_PDA_SEED: &str = "CONSTITUENT_VAULT"; +pub const LP_POOL_TOKEN_VAULT_PDA_SEED: &str = "LP_POOL_TOKEN_VAULT"; + +pub const BASE_SWAP_FEE: i128 = 300; // 0.75% in PERCENTAGE_PRECISION +pub const MAX_SWAP_FEE: i128 = 75_000; // 0.75% in PERCENTAGE_PRECISION +pub const MIN_SWAP_FEE: i128 = 200; // 0.75% in PERCENTAGE_PRECISION + +pub const MIN_AUM_EXECUTION_FEE: u128 = 10_000_000_000_000; + +// Delay constants +#[cfg(feature = "anchor-test")] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 100; +#[cfg(not(feature = "anchor-test"))] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 10; +pub const LP_POOL_SWAP_AUM_UPDATE_DELAY: u64 = 0; +#[cfg(feature = "anchor-test")] +pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +#[cfg(not(feature = "anchor-test"))] +pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 0u64; + +#[cfg(feature = "anchor-test")] +pub const MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +#[cfg(not(feature = "anchor-test"))] +pub const MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10u64; + +#[cfg(test)] +mod tests; + +#[account(zero_copy(unsafe))] +#[derive(Default, Debug)] +#[repr(C)] +pub struct LPPool { + /// name of vault, TODO: check type + size + pub name: [u8; 32], + /// address of the vault. + pub pubkey: Pubkey, + // vault token mint + pub mint: Pubkey, // 32, 96 + // whitelist mint + pub whitelist_mint: Pubkey, + // constituent target base pubkey + pub constituent_target_base: Pubkey, + // constituent correlations pubkey + pub constituent_correlations: Pubkey, + + /// The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index) + /// which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0) + /// pub quote_constituent_index: u16, + + /// QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this + pub max_aum: u128, + + /// QUOTE_PRECISION: AUM of the vault in USD, updated lazily + pub last_aum: u128, + + /// QUOTE PRECISION: Cumulative quotes from settles + pub cumulative_quote_sent_to_perp_markets: u128, + pub cumulative_quote_received_from_perp_markets: u128, + + /// QUOTE_PRECISION: Total fees paid for minting and redeeming LP tokens + pub total_mint_redeem_fees_paid: i128, + + /// timestamp of last AUM slot + pub last_aum_slot: u64, + + pub max_settle_quote_amount: u64, + + /// timestamp of last vAMM revenue rebalance + pub last_hedge_ts: u64, + + /// Every mint/redeem has a monotonically increasing id. This is the next id to use + pub mint_redeem_id: u64, + pub settle_id: u64, + + /// PERCENTAGE_PRECISION + pub min_mint_fee: i64, + pub token_supply: u64, + + // PERCENTAGE_PRECISION: percentage precision const = 100% + pub volatility: u64, + + pub constituents: u16, + pub quote_consituent_index: u16, + + pub bump: u8, + + // No precision - just constant + pub gamma_execution: u8, + // No precision - just constant + pub xi: u8, + + pub padding: u8, +} + +impl Size for LPPool { + const SIZE: usize = 376; +} + +impl LPPool { + pub fn sync_token_supply(&mut self, supply: u64) { + self.token_supply = supply; + } + + pub fn get_price(&self, mint_supply: u64) -> Result { + match mint_supply { + 0 => Ok(0), + supply => { + // TODO: assuming mint decimals = quote decimals = 6 + Ok(self + .last_aum + .safe_mul(PRICE_PRECISION)? + .safe_div(supply as u128)?) + } + } + } + + /// Get the swap price between two (non-LP token) constituents. + /// Accounts for precision differences between in and out constituents + /// returns swap price in PRICE_PRECISION + pub fn get_swap_price( + &self, + in_decimals: u32, + out_decimals: u32, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + ) -> DriftResult<(u128, u128)> { + let in_price = in_oracle.price.cast::()?; + let out_price = out_oracle.price.cast::()?; + + let (prec_diff_numerator, prec_diff_denominator) = if out_decimals > in_decimals { + (10_u128.pow(out_decimals - in_decimals), 1) + } else { + (1, 10_u128.pow(in_decimals - out_decimals)) + }; + + let swap_price_num = in_price.safe_mul(prec_diff_numerator)?; + let swap_price_denom = out_price.safe_mul(prec_diff_denominator)?; + + Ok((swap_price_num, swap_price_denom)) + } + + /// in the respective token units. Amounts are gross fees and in + /// token mint precision. + /// Positive fees are paid, negative fees are rebated + /// Returns (in_amount out_amount, in_fee, out_fee) + pub fn get_swap_amount( + &self, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + in_constituent: &Constituent, + out_constituent: &Constituent, + in_spot_market: &SpotMarket, + out_spot_market: &SpotMarket, + in_target_weight: i64, + out_target_weight: i64, + in_amount: u128, + correlation: i64, + ) -> DriftResult<(u128, u128, i128, i128)> { + let (swap_price_num, swap_price_denom) = self.get_swap_price( + in_spot_market.decimals, + out_spot_market.decimals, + in_oracle, + out_oracle, + )?; + + let (in_fee, out_fee) = self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + Some(out_spot_market), + Some(out_oracle.price), + Some(out_constituent), + Some(out_target_weight), + correlation, + )?; + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let out_amount = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .cast::()? + .safe_mul(swap_price_num)? + .safe_div(swap_price_denom)?; + + let out_fee_amount = out_amount + .cast::()? + .safe_mul(out_fee as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + Ok((in_amount, out_amount, in_fee_amount, out_fee_amount)) + } + + /// Calculates the amount of LP tokens to mint for a given input of constituent tokens. + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_add_liquidity_mint_amount( + &self, + in_spot_market: &SpotMarket, + in_constituent: &Constituent, + in_amount: u128, + in_oracle: &OraclePriceData, + in_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let (in_fee_pct, out_fee_pct) = if self.last_aum == 0 { + (0, 0) + } else { + self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + None, + None, + None, + None, + 0, + )? + }; + let in_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee_pct)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let in_amount_less_fees = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .max(0) + .cast::()?; + + let token_precision_denominator = 10_u128.pow(in_spot_market.decimals); + let token_amount_usd = in_oracle + .price + .cast::()? + .safe_mul(in_amount_less_fees)?; + let lp_amount = if self.last_aum == 0 { + token_amount_usd.safe_div(token_precision_denominator)? + } else { + token_amount_usd + .safe_mul(dlp_total_supply.max(1) as u128)? + .safe_div(self.last_aum)? + .safe_div(token_precision_denominator)? + }; + + let lp_fee_to_charge_pct = self.min_mint_fee; + // let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, true)?; + let lp_fee_to_charge = lp_amount + .safe_mul(lp_fee_to_charge_pct as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .cast::()?; + + Ok(( + lp_amount.cast::()?, + in_amount, + lp_fee_to_charge, + in_fee_amount, + )) + } + + /// Calculates the amount of constituent tokens to receive for a given amount of LP tokens to burn + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_remove_liquidity_amount( + &self, + out_spot_market: &SpotMarket, + out_constituent: &Constituent, + lp_burn_amount: u64, + out_oracle: &OraclePriceData, + out_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let lp_fee_to_charge_pct = self.min_mint_fee; + // let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, false)?; + let lp_fee_to_charge = lp_burn_amount + .cast::()? + .safe_mul(lp_fee_to_charge_pct.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .cast::()?; + + let lp_amount_less_fees = (lp_burn_amount as i128).safe_sub(lp_fee_to_charge as i128)?; + + let token_precision_denominator = 10_u128.pow(out_spot_market.decimals); + + // Calculate proportion of LP tokens being burned + let proportion = lp_amount_less_fees + .cast::()? + .safe_mul(10u128.pow(3))? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(dlp_total_supply as u128)?; + + // Apply proportion to AUM and convert to token amount + let out_amount = self + .last_aum + .safe_mul(proportion)? + .safe_mul(token_precision_denominator)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(10u128.pow(3))? + .safe_div(out_oracle.price.cast::()?)?; + + let (in_fee_pct, out_fee_pct) = self.get_swap_fees( + out_spot_market, + out_oracle.price, + out_constituent, + out_amount.cast::()?.safe_mul(-1_i128)?, + out_target_weight, + None, + None, + None, + None, + 0, + )?; + let out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + let out_fee_amount = out_amount + .cast::()? + .safe_mul(out_fee_pct)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + Ok((lp_burn_amount, out_amount, lp_fee_to_charge, out_fee_amount)) + } + + pub fn get_quadratic_fee_inventory( + &self, + gamma_covar: [[i128; 2]; 2], + pre_notional_errors: [i128; 2], + post_notional_errors: [i128; 2], + trade_notional: u128, + ) -> DriftResult<(i128, i128)> { + let gamma_covar_error_pre_in = gamma_covar[0][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_pre_out = gamma_covar[1][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let gamma_covar_error_post_in = gamma_covar[0][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_post_out = gamma_covar[1][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let c_pre_in: i128 = gamma_covar_error_pre_in + .safe_mul(pre_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_pre_out = gamma_covar_error_pre_out + .safe_mul(pre_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let c_post_in: i128 = gamma_covar_error_post_in + .safe_mul(post_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_post_out = gamma_covar_error_post_out + .safe_mul(post_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let in_fee = c_post_in + .safe_sub(c_pre_in)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional.cast::()?)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + let out_fee = c_post_out + .safe_sub(c_pre_out)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional.cast::()?)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + + Ok((in_fee, out_fee)) + } + + pub fn get_linear_fee_execution( + &self, + trade_ratio: i128, + kappa_execution: u128, + xi: u8, + ) -> DriftResult { + trade_ratio + .safe_mul(kappa_execution.safe_mul(xi as u128)?.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + pub fn get_quadratic_fee_execution( + &self, + trade_ratio: i128, + kappa_execution: u128, + xi: u8, + ) -> DriftResult { + kappa_execution + .cast::()? + .safe_mul(xi.safe_mul(xi)?.cast::()?)? + .safe_mul(trade_ratio.safe_mul(trade_ratio)?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + /// returns fee in PERCENTAGE_PRECISION + pub fn get_swap_fees( + &self, + in_spot_market: &SpotMarket, + in_oracle_price: i64, + in_constituent: &Constituent, + in_amount: i128, + in_target_weight: i64, + out_spot_market: Option<&SpotMarket>, + out_oracle_price: Option, + out_constituent: Option<&Constituent>, + out_target_weight: Option, + correlation: i64, + ) -> DriftResult<(i128, i128)> { + let notional_trade_size = in_constituent.get_notional(in_oracle_price, in_amount)?; + let in_volatility = in_constituent.volatility; + + let ( + mint_redeem, + out_volatility, + out_gamma_execution, + out_gamma_inventory, + out_xi, + out_notional_target, + out_notional_pre, + out_notional_post, + ) = if let Some(out_constituent) = out_constituent { + let out_spot_market = out_spot_market.unwrap(); + let out_oracle_price = out_oracle_price.unwrap(); + let out_amount = notional_trade_size + .safe_mul(10_i128.pow(out_spot_market.decimals as u32))? + .safe_div(out_oracle_price.cast::()?)?; + ( + false, + out_constituent.volatility, + out_constituent.gamma_execution, + out_constituent.gamma_inventory, + out_constituent.xi, + out_target_weight + .unwrap() + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?, + out_constituent.get_notional_with_delta(out_oracle_price, out_spot_market, 0)?, + out_constituent.get_notional_with_delta( + out_oracle_price, + out_spot_market, + out_amount.safe_mul(-1)?, + )?, + ) + } else { + ( + true, + self.volatility, + self.gamma_execution, + 0, + self.xi, + 0, + 0, + 0, + ) + }; + + let in_kappa_execution: u128 = (in_volatility as u128) + .safe_mul(in_volatility as u128)? + .safe_mul(in_constituent.gamma_execution as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(2u128)?; + + let out_kappa_execution: u128 = (out_volatility as u128) + .safe_mul(out_volatility as u128)? + .safe_mul(out_gamma_execution as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(2u128)?; + + // Compute notional targets and errors + let in_notional_target = in_target_weight + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let in_notional_pre = + in_constituent.get_notional_with_delta(in_oracle_price, in_spot_market, 0)?; + let in_notional_post = + in_constituent.get_notional_with_delta(in_oracle_price, in_spot_market, in_amount)?; + let in_notional_error_pre = in_notional_pre.safe_sub(in_notional_target)?; + + // keep aum fixed if it's a swap for calculating post error, othwerise + // increase aum first + let in_notional_error_post = if !mint_redeem { + in_notional_post.safe_sub(in_notional_target)? + } else { + let adjusted_aum = self + .last_aum + .cast::()? + .safe_add(notional_trade_size)?; + let in_notional_target_post_mint_redeem = in_target_weight + .cast::()? + .safe_mul(adjusted_aum)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + in_notional_post.safe_sub(in_notional_target_post_mint_redeem)? + }; + + let out_notional_error_pre = out_notional_pre.safe_sub(out_notional_target)?; + let out_notional_error_post = out_notional_post.safe_sub(out_notional_target)?; + + let trade_ratio: i128 = notional_trade_size + .abs() + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(self.last_aum.max(MIN_AUM_EXECUTION_FEE).cast::()?)?; + + // Linear fee computation amount + let in_fee_execution_linear = + self.get_linear_fee_execution(trade_ratio, in_kappa_execution, in_constituent.xi)?; + + let out_fee_execution_linear = + self.get_linear_fee_execution(trade_ratio, out_kappa_execution, out_xi)?; + + // Quadratic fee components + let in_fee_execution_quadratic = + self.get_quadratic_fee_execution(trade_ratio, in_kappa_execution, in_constituent.xi)?; + let out_fee_execution_quadratic = + self.get_quadratic_fee_execution(trade_ratio, out_kappa_execution, out_xi)?; + let (in_quadratic_inventory_fee, out_quadratic_inventory_fee) = self + .get_quadratic_fee_inventory( + get_gamma_covar_matrix( + correlation, + in_constituent.gamma_inventory, + out_gamma_inventory, + in_constituent.volatility, + out_volatility, + )?, + [in_notional_error_pre, out_notional_error_pre], + [in_notional_error_post, out_notional_error_post], + notional_trade_size.abs().cast::()?, + )?; + + msg!( + "fee breakdown - in_exec_linear: {}, in_exec_quad: {}, in_inv_quad: {}, out_exec_linear: {}, out_exec_quad: {}, out_inv_quad: {}", + in_fee_execution_linear, + in_fee_execution_quadratic, + in_quadratic_inventory_fee, + out_fee_execution_linear, + out_fee_execution_quadratic, + out_quadratic_inventory_fee + ); + let total_in_fee = in_fee_execution_linear + .safe_add(in_fee_execution_quadratic)? + .safe_add(in_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + let total_out_fee = out_fee_execution_linear + .safe_add(out_fee_execution_quadratic)? + .safe_add(out_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + + Ok(( + total_in_fee.min(MAX_SWAP_FEE.safe_div(2)?), + total_out_fee.min(MAX_SWAP_FEE.safe_div(2)?), + )) + } + + pub fn record_mint_redeem_fees(&mut self, amount: i64) -> DriftResult { + self.total_mint_redeem_fees_paid = self + .total_mint_redeem_fees_paid + .safe_add(amount.cast::()?)?; + Ok(()) + } + + pub fn update_aum( + &mut self, + slot: u64, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + constituent_target_base: &AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, + amm_cache: &AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed>, + ) -> DriftResult<(u128, i128, BTreeMap>)> { + let mut aum: i128 = 0; + let mut crypto_delta = 0_i128; + let mut derivative_groups: BTreeMap> = BTreeMap::new(); + for i in 0..self.constituents as usize { + let constituent = constituent_map.get_ref(&(i as u16))?; + if slot.saturating_sub(constituent.last_oracle_slot) + > constituent.oracle_staleness_threshold + { + msg!( + "Constituent {} oracle slot is too stale: {}, current slot: {}", + constituent.constituent_index, + constituent.last_oracle_slot, + slot + ); + return Err(ErrorCode::ConstituentOracleStale.into()); + } + + if constituent.constituent_derivative_index >= 0 && constituent.derivative_weight != 0 { + if !derivative_groups + .contains_key(&(constituent.constituent_derivative_index as u16)) + { + derivative_groups.insert( + constituent.constituent_derivative_index as u16, + vec![constituent.constituent_index], + ); + } else { + derivative_groups + .get_mut(&(constituent.constituent_derivative_index as u16)) + .unwrap() + .push(constituent.constituent_index); + } + } + + let spot_market = spot_market_map.get_ref(&constituent.spot_market_index)?; + + let constituent_aum = constituent + .get_full_token_amount(&spot_market)? + .safe_mul(constituent.last_oracle_price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))?; + msg!( + "constituent: {}, balance: {}, aum: {}, deriv index: {}, bl token balance {}, bl balance type {}, vault balance: {}", + constituent.constituent_index, + constituent.get_full_token_amount(&spot_market)?, + constituent_aum, + constituent.constituent_derivative_index, + constituent.spot_balance.get_token_amount(&spot_market)?, + constituent.spot_balance.balance_type, + constituent.vault_token_balance + ); + + // sum up crypto deltas (notional exposures for all non-stablecoins) + if constituent.constituent_index != self.quote_consituent_index + && constituent.constituent_derivative_index != self.quote_consituent_index as i16 + { + let constituent_target_notional = constituent_target_base + .get(constituent.constituent_index as u32) + .target_base + .cast::()? + .safe_mul(constituent.last_oracle_price.cast::()?)? + .safe_div(10_i128.pow(constituent.decimals as u32))? + .cast::()?; + crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?; + } + aum = aum.safe_add(constituent_aum)?; + } + + msg!("Aum before quote owed from lp pool: {}", aum); + + for cache_datum in amm_cache.iter() { + aum = aum.saturating_sub(cache_datum.quote_owed_from_lp_pool as i128); + } + + let aum_u128 = aum.max(0i128).cast::()?; + self.last_aum = aum_u128; + self.last_aum_slot = slot; + + Ok((aum_u128, crypto_delta, derivative_groups)) + } + + pub fn get_lp_pool_signer_seeds<'a>(name: &'a [u8; 32], bump: &'a u8) -> [&'a [u8]; 3] { + [LP_POOL_PDA_SEED.as_ref(), name, bytemuck::bytes_of(bump)] + } +} + +#[zero_copy(unsafe)] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct ConstituentSpotBalance { + /// The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow + /// interest of corresponding market. + /// precision: token precision + pub scaled_balance: u128, + /// The cumulative deposits/borrows a user has made into a market + /// precision: token mint precision + pub cumulative_deposits: i64, + /// The market index of the corresponding spot market + pub market_index: u16, + /// Whether the position is deposit or borrow + pub balance_type: SpotBalanceType, + pub padding: [u8; 5], +} + +impl SpotBalance for ConstituentSpotBalance { + fn market_index(&self) -> u16 { + self.market_index + } + + fn balance_type(&self) -> &SpotBalanceType { + &self.balance_type + } + + fn balance(&self) -> u128 { + self.scaled_balance as u128 + } + + fn increase_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_add(delta)?; + Ok(()) + } + + fn decrease_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_sub(delta)?; + Ok(()) + } + + fn update_balance_type(&mut self, balance_type: SpotBalanceType) -> DriftResult { + self.balance_type = balance_type; + Ok(()) + } +} + +impl ConstituentSpotBalance { + pub fn get_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + get_token_amount(self.scaled_balance, spot_market, &self.balance_type) + } + + pub fn get_signed_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + let token_amount = self.get_token_amount(spot_market)?; + get_signed_token_amount(token_amount, &self.balance_type) + } +} + +#[account(zero_copy(unsafe))] +#[derive(Default, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct Constituent { + /// address of the constituent + pub pubkey: Pubkey, + pub mint: Pubkey, + pub lp_pool: Pubkey, + pub vault: Pubkey, + + /// total fees received by the constituent. Positive = fees received, Negative = fees paid + pub total_swap_fees: i128, + + /// spot borrow-lend balance for constituent + pub spot_balance: ConstituentSpotBalance, // should be in constituent base asset + + /// max deviation from target_weight allowed for the constituent + /// precision: PERCENTAGE_PRECISION + pub max_weight_deviation: i64, + /// min fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_min: i64, + /// max fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_max: i64, + + /// Max Borrow amount: + /// precision: token precision + pub max_borrow_token_amount: u64, + + /// ata token balance in token precision + pub vault_token_balance: u64, + + pub last_oracle_price: i64, + pub last_oracle_slot: u64, + + pub oracle_staleness_threshold: u64, + + pub flash_loan_initial_token_amount: u64, + /// Every swap to/from this constituent has a monotonically increasing id. This is the next id to use + pub next_swap_id: u64, + + /// percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight + pub derivative_weight: u64, + + pub volatility: u64, // volatility in PERCENTAGE_PRECISION 1=1% + + // depeg threshold in relation top parent in PERCENTAGE_PRECISION + pub constituent_derivative_depeg_threshold: u64, + + /// The `constituent_index` of the parent constituent. -1 if it is a parent index + /// Example: if in a pool with SOL (parent) and dSOL (derivative), + /// SOL.constituent_index = 1, SOL.constituent_derivative_index = -1, + /// dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1 + pub constituent_derivative_index: i16, + + pub spot_market_index: u16, + pub constituent_index: u16, + + pub decimals: u8, + pub bump: u8, + pub vault_bump: u8, + + // Fee params + pub gamma_inventory: u8, + pub gamma_execution: u8, + pub xi: u8, + + // Status + pub status: u8, + pub paused_operations: u8, + pub _padding: [u8; 2], +} + +impl Size for Constituent { + const SIZE: usize = 304; +} + +#[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentStatus { + /// fills only able to reduce liability + ReduceOnly = 0b00000001, + /// market has no remaining participants + Decommissioned = 0b00000010, +} + +impl Constituent { + pub fn get_status(&self) -> DriftResult> { + BitFlags::::from_bits(usize::from(self.status)).safe_unwrap() + } + + pub fn is_decommissioned(&self) -> DriftResult { + Ok(self + .get_status()? + .contains(ConstituentStatus::Decommissioned)) + } + + pub fn is_reduce_only(&self) -> DriftResult { + Ok(self.get_status()?.contains(ConstituentStatus::ReduceOnly)) + } + + pub fn does_constituent_allow_operation( + &self, + operation: ConstituentLpOperation, + ) -> DriftResult<()> { + if self.is_decommissioned()? { + msg!( + "Constituent {:?}, spot market {}, is decommissioned", + self.pubkey, + self.spot_market_index + ); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } else if ConstituentLpOperation::is_operation_paused(self.paused_operations, operation) { + msg!( + "Constituent {:?}, spot market {}, is paused for operation {:?}", + self.pubkey, + self.spot_market_index, + operation + ); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } else { + Ok(()) + } + } + + pub fn is_operation_reducing( + &self, + spot_market: &SpotMarket, + is_increasing: bool, + ) -> DriftResult { + let current_balance_sign = self.get_full_token_amount(spot_market)?.signum(); + if current_balance_sign > 0 { + Ok(!is_increasing) + } else { + Ok(is_increasing) + } + } + + /// Returns the full balance of the Constituent, the total of the amount in Constituent's token + /// account and in Drift Borrow-Lend. + pub fn get_full_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + let token_amount = self.spot_balance.get_signed_token_amount(spot_market)?; + let vault_balance = self.vault_token_balance.cast::()?; + token_amount.safe_add(vault_balance) + } + + pub fn record_swap_fees(&mut self, amount: i128) -> DriftResult { + self.total_swap_fees = self.total_swap_fees.safe_add(amount)?; + Ok(()) + } + + /// Current weight of this constituent = price * token_balance / lp_pool_aum + /// Note: lp_pool_aum is from LPPool.last_aum, which is a lagged value updated via crank + pub fn get_weight( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount_delta: i128, + lp_pool_aum: u128, + ) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let value_usd = self.get_notional_with_delta(price, spot_market, token_amount_delta)?; + + value_usd + .safe_mul(PERCENTAGE_PRECISION_I64.cast::()?)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::() + } + + pub fn get_notional(&self, price: i64, token_amount: i128) -> DriftResult { + let token_precision = 10_i128.pow(self.decimals as u32); + let value_usd = token_amount.safe_mul(price.cast::()?)?; + value_usd.safe_div(token_precision) + } + + pub fn get_notional_with_delta( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount: i128, + ) -> DriftResult { + let token_precision = 10_i128.pow(self.decimals as u32); + let balance = self.get_full_token_amount(spot_market)?.cast::()?; + let amount = balance.safe_add(token_amount)?; + let value_usd = amount.safe_mul(price.cast::()?)?; + value_usd.safe_div(token_precision) + } + + pub fn sync_token_balance(&mut self, token_account_amount: u64) { + self.vault_token_balance = token_account_amount; + } + + pub fn get_vault_signer_seeds<'a>( + lp_pool: &'a Pubkey, + spot_market_index: &'a u16, + bump: &'a u8, + ) -> [&'a [u8]; 4] { + [ + CONSTITUENT_VAULT_PDA_SEED.as_ref(), + lp_pool.as_ref(), + bytemuck::bytes_of(spot_market_index), + bytemuck::bytes_of(bump), + ] + } + + pub fn get_max_transfer(&self, spot_market: &SpotMarket) -> DriftResult { + let token_amount = self.get_full_token_amount(spot_market)?; + + let max_transfer = if self.spot_balance.balance_type == SpotBalanceType::Borrow { + self.max_borrow_token_amount + .saturating_sub(token_amount as u64) + } else { + self.max_borrow_token_amount + .saturating_add(token_amount as u64) + }; + + Ok(max_transfer) + } +} + +#[zero_copy] +#[derive(Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct AmmConstituentDatum { + pub perp_market_index: u16, + pub constituent_index: u16, + pub _padding: [u8; 4], + pub last_slot: u64, + /// PERCENTAGE_PRECISION. The weight this constituent has on the perp market + pub weight: i64, +} + +impl Default for AmmConstituentDatum { + fn default() -> Self { + AmmConstituentDatum { + perp_market_index: u16::MAX, + constituent_index: u16::MAX, + _padding: [0; 4], + last_slot: 0, + weight: 0, + } + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct AmmConstituentMappingFixed { + pub lp_pool: Pubkey, + pub bump: u8, + pub _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmConstituentMappingFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmConstituentMapping { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. Each datum represents the target weight for a single (AMM, Constituent) pair. + // An AMM may be partially backed by multiple Constituents + pub weights: Vec, +} + +impl AmmConstituentMapping { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 24 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.weights.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + Ok(()) + } + + pub fn sort(&mut self) { + self.weights.sort_by_key(|datum| datum.constituent_index); + } +} + +impl_zero_copy_loader!( + AmmConstituentMapping, + crate::id, + AmmConstituentMappingFixed, + AmmConstituentDatum +); + +#[zero_copy] +#[derive(Debug, Default, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct TargetsDatum { + pub cost_to_trade_bps: i32, + pub _padding: [u8; 4], + pub last_slot: u64, + pub target_base: i64, +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentTargetBaseFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentTargetBaseFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentTargetBase { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub targets: Vec, +} + +impl ConstituentTargetBase { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 24 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.targets.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + + validate!( + !self.targets.iter().any(|t| t.cost_to_trade_bps == 0), + ErrorCode::DefaultError, + "cost_to_trade_bps must be non-zero" + )?; + + Ok(()) + } +} + +impl_zero_copy_loader!( + ConstituentTargetBase, + crate::id, + ConstituentTargetBaseFixed, + TargetsDatum +); + +impl Default for ConstituentTargetBase { + fn default() -> Self { + ConstituentTargetBase { + lp_pool: Pubkey::default(), + bump: 0, + _padding: [0; 3], + targets: Vec::with_capacity(0), + } + } +} + +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum WeightValidationFlags { + NONE = 0b0000_0000, + EnforceTotalWeight100 = 0b0000_0001, + NoNegativeWeights = 0b0000_0010, + NoOverweight = 0b0000_0100, +} + +impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn get_target_weight( + &self, + constituent_index: u16, + spot_market: &SpotMarket, + price: i64, + aum: u128, + ) -> DriftResult { + validate!( + constituent_index < self.len() as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index = {}, ConstituentTargetBase len = {}", + constituent_index, + self.len() + )?; + + // TODO: validate spot market + let datum = self.get(constituent_index as u32); + let target_weight = calculate_target_weight(datum.target_base, &spot_market, price, aum)?; + Ok(target_weight) + } +} + +pub fn calculate_target_weight( + target_base: i64, + spot_market: &SpotMarket, + price: i64, + lp_pool_aum: u128, +) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let notional: i128 = (target_base as i128) + .safe_mul(price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))?; + + let target_weight = notional + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::()? + .clamp(-1 * PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_I64); + + Ok(target_weight) +} + +/// Update target base based on amm_inventory and mapping +pub struct AmmInventoryAndPrices { + pub inventory: i64, + pub price: i64, +} + +pub struct ConstituentIndexAndDecimalAndPrice { + pub constituent_index: u16, + pub decimals: u8, + pub price: i64, +} + +impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn update_target_base( + &mut self, + mapping: &AccountZeroCopy<'a, AmmConstituentDatum, AmmConstituentMappingFixed>, + amm_inventory_and_prices: &[AmmInventoryAndPrices], + constituents_indexes_and_decimals_and_prices: &mut [ConstituentIndexAndDecimalAndPrice], + slot: u64, + ) -> DriftResult<()> { + // Sorts by constituent index + constituents_indexes_and_decimals_and_prices.sort_by_key(|c| c.constituent_index); + + // Precompute notional by perp market index + let mut notionals: Vec = Vec::with_capacity(amm_inventory_and_prices.len()); + for &AmmInventoryAndPrices { inventory, price } in amm_inventory_and_prices.iter() { + let notional = (inventory as i128) + .safe_mul(price as i128)? + .safe_div(BASE_PRECISION_I128)?; + notionals.push(notional); + } + + let mut mapping_index = 0; + for ( + i, + &ConstituentIndexAndDecimalAndPrice { + constituent_index, + decimals, + price, + }, + ) in constituents_indexes_and_decimals_and_prices + .iter() + .enumerate() + { + let mut target_notional = 0i128; + + let mut j = mapping_index; + while j < mapping.len() { + let d = mapping.get(j); + if d.constituent_index != constituent_index { + while j < mapping.len() && mapping.get(j).constituent_index < constituent_index + { + j += 1; + } + break; + } + if let Some(perp_notional) = notionals.get(d.perp_market_index as usize) { + target_notional = target_notional + .saturating_add(perp_notional.saturating_mul(d.weight as i128)); + } + j += 1; + } + mapping_index = j; + + let cell = self.get_mut(i as u32); + let target_base = target_notional + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul(10_i128.pow(decimals as u32))? + .safe_div(price as i128)? + * -1; // Want to target opposite sign of total scaled notional inventory + + msg!( + "updating constituent index {} target base to {} from aggregated perp notional {}", + constituent_index, + target_base, + target_notional, + ); + cell.target_base = target_base.cast::()?; + cell.last_slot = slot; + } + + Ok(()) + } +} + +impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> { + #[cfg(test)] + pub fn add_amm_constituent_datum(&mut self, datum: AmmConstituentDatum) -> DriftResult<()> { + let len = self.len(); + + let mut open_slot_index: Option = None; + for i in 0..len { + let cell = self.get(i as u32); + if cell.constituent_index == datum.constituent_index + && cell.perp_market_index == datum.perp_market_index + { + return Err(ErrorCode::DefaultError); + } + if cell.last_slot == 0 && open_slot_index.is_none() { + open_slot_index = Some(i); + } + } + let open_slot = open_slot_index.ok_or_else(|| ErrorCode::DefaultError.into())?; + + let cell = self.get_mut(open_slot); + *cell = datum; + + self.sort()?; + Ok(()) + } + + #[cfg(test)] + pub fn sort(&mut self) -> DriftResult<()> { + let len = self.len(); + let mut data: Vec = Vec::with_capacity(len as usize); + for i in 0..len { + data.push(*self.get(i as u32)); + } + data.sort_by_key(|datum| datum.constituent_index); + for i in 0..len { + let cell = self.get_mut(i as u32); + *cell = data[i as usize]; + } + Ok(()) + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentCorrelationsFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentCorrelationsFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentCorrelations { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub correlations: Vec, +} + +impl HasLen for ConstituentCorrelations { + fn len(&self) -> u32 { + self.correlations.len() as u32 + } +} + +impl_zero_copy_loader!( + ConstituentCorrelations, + crate::id, + ConstituentCorrelationsFixed, + i64 +); + +impl ConstituentCorrelations { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * num_constituents * 8 + } + + pub fn validate(&self) -> DriftResult<()> { + let len = self.correlations.len(); + let num_constituents = (len as f32).sqrt() as usize; // f32 is plenty precise for matrix dims < 2^16 + validate!( + num_constituents * num_constituents == self.correlations.len(), + ErrorCode::DefaultError, + "ConstituentCorrelation correlations len must be a perfect square" + )?; + + for i in 0..num_constituents { + for j in 0..num_constituents { + let corr = self.correlations[i * num_constituents + j]; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + let corr_ji = self.correlations[j * num_constituents + i]; + validate!( + corr == corr_ji, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be symmetric" + )?; + } + let corr_ii = self.correlations[i * num_constituents + i]; + validate!( + corr_ii == PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations diagonal must be PERCENTAGE_PRECISION" + )?; + } + + Ok(()) + } + + pub fn add_new_constituent(&mut self, new_constituent_correlations: &[i64]) -> DriftResult { + // Add a new constituent at index N (where N = old size), + // given a slice `new_corrs` of length `N` such that + // new_corrs[i] == correlation[i, N]. + // + // On entry: + // self.correlations.len() == N*N + // + // After: + // self.correlations.len() == (N+1)*(N+1) + let len = self.correlations.len(); + let n = (len as f64).sqrt() as usize; + validate!( + n * n == len, + ErrorCode::DefaultError, + "existing correlations len must be a perfect square" + )?; + validate!( + new_constituent_correlations.len() == n, + ErrorCode::DefaultError, + "new_corrs length must equal number of number of other constituents ({})", + n + )?; + for &c in new_constituent_correlations { + validate!( + c <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "correlation must be ≤ PERCENTAGE_PRECISION" + )?; + } + + let new_n = n + 1; + let mut buf = Vec::with_capacity(new_n * new_n); + + for i in 0..n { + buf.extend_from_slice(&self.correlations[i * n..i * n + n]); + buf.push(new_constituent_correlations[i]); + } + + buf.extend_from_slice(new_constituent_correlations); + buf.push(PERCENTAGE_PRECISION_I64); + + self.correlations = buf; + + debug_assert_eq!(self.correlations.len(), new_n * new_n); + + Ok(()) + } + + pub fn set_correlation(&mut self, i: u16, j: u16, corr: i64) -> DriftResult { + let num_constituents = (self.correlations.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + + self.correlations[(i as usize * num_constituents + j as usize) as usize] = corr; + self.correlations[(j as usize * num_constituents + i as usize) as usize] = corr; + + self.validate()?; + + Ok(()) + } +} + +impl<'a> AccountZeroCopy<'a, i64, ConstituentCorrelationsFixed> { + pub fn get_correlation(&self, i: u16, j: u16) -> DriftResult { + let num_constituents = (self.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + + let corr = self.get((i as usize * num_constituents + j as usize) as u32); + Ok(*corr) + } +} + +pub fn get_gamma_covar_matrix( + correlation_ij: i64, + gamma_i: u8, + gamma_j: u8, + vol_i: u64, + vol_j: u64, +) -> DriftResult<[[i128; 2]; 2]> { + // Build the covariance matrix + let mut covar_matrix = [[0i128; 2]; 2]; + let scaled_vol_i = vol_i as i128; + let scaled_vol_j = vol_j as i128; + covar_matrix[0][0] = scaled_vol_i + .safe_mul(scaled_vol_i)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][1] = scaled_vol_j + .safe_mul(scaled_vol_j)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[0][1] = scaled_vol_i + .safe_mul(scaled_vol_j)? + .safe_mul(correlation_ij as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][0] = covar_matrix[0][1]; + + // Build the gamma matrix as a diagonal matrix + let gamma_matrix = [[gamma_i as i128, 0i128], [0i128, gamma_j as i128]]; + + // Multiply gamma_matrix with covar_matrix: product = gamma_matrix * covar_matrix + let mut product = [[0i128; 2]; 2]; + for i in 0..2 { + for j in 0..2 { + for k in 0..2 { + product[i][j] = product[i][j] + .checked_add( + gamma_matrix[i][k] + .checked_mul(covar_matrix[k][j]) + .ok_or(ErrorCode::MathError)?, + ) + .ok_or(ErrorCode::MathError)?; + } + } + } + + Ok(product) +} + +pub fn update_constituent_target_base_for_derivatives( + aum: u128, + derivative_groups: &BTreeMap>, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + constituent_target_base: &mut AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, +) -> DriftResult<()> { + for (parent_index, constituent_indexes) in derivative_groups.iter() { + let parent_constituent = constituent_map.get_ref(&(parent_index))?; + let parent_target_base = constituent_target_base + .get(*parent_index as u32) + .target_base; + let target_parent_weight = calculate_target_weight( + parent_target_base, + &*spot_market_map.get_ref(&parent_constituent.spot_market_index)?, + parent_constituent.last_oracle_price, + aum, + )?; + let mut derivative_weights_sum: u64 = 0; + for constituent_index in constituent_indexes { + let constituent = constituent_map.get_ref(constituent_index)?; + if constituent.last_oracle_price + < parent_constituent + .last_oracle_price + .safe_mul(constituent.constituent_derivative_depeg_threshold as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)? + { + msg!( + "Constituent {} last oracle price {} is too low compared to parent constituent {} last oracle price {}. Assuming depegging and setting target base to 0.", + constituent.constituent_index, + constituent.last_oracle_price, + parent_constituent.constituent_index, + parent_constituent.last_oracle_price + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = 0_i64; + continue; + } + + derivative_weights_sum = + derivative_weights_sum.saturating_add(constituent.derivative_weight); + + let target_weight = (target_parent_weight as i128) + .safe_mul(constituent.derivative_weight.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + msg!( + "constituent: {}, target weight: {}", + constituent_index, + target_weight, + ); + let target_base = aum + .cast::()? + .safe_mul(target_weight)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul(10_i128.pow(constituent.decimals as u32))? + .safe_div(constituent.last_oracle_price as i128)?; + + msg!( + "constituent: {}, target base: {}", + constituent_index, + target_base + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = target_base.cast::()?; + } + + validate!( + derivative_weights_sum <= PERCENTAGE_PRECISION_U64, + ErrorCode::InvalidConstituentDerivativeWeights, + "derivative_weights_sum for parent constituent {} must be less than or equal to 100%", + parent_index + )?; + + constituent_target_base + .get_mut(*parent_index as u32) + .target_base = parent_target_base + .safe_mul(PERCENTAGE_PRECISION_U64.safe_sub(derivative_weights_sum)? as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)?; + } + + Ok(()) +} diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs new file mode 100644 index 0000000000..f872dd7616 --- /dev/null +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -0,0 +1,3436 @@ +#[cfg(test)] +mod tests { + use crate::math::constants::{ + BASE_PRECISION_I64, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + use std::{cell::RefCell, marker::PhantomData, vec}; + + fn amm_const_datum( + perp_market_index: u16, + constituent_index: u16, + weight: i64, + last_slot: u64, + ) -> AmmConstituentDatum { + AmmConstituentDatum { + perp_market_index, + constituent_index, + weight, + last_slot, + ..AmmConstituentDatum::default() + } + } + + #[test] + fn test_complex_implementation() { + // Constituents are BTC, SOL, ETH, USDC + + let slot = 20202020 as u64; + let amm_data = [ + amm_const_datum(0, 0, PERCENTAGE_PRECISION_I64, slot), // BTC-PERP + amm_const_datum(1, 1, PERCENTAGE_PRECISION_I64, slot), // SOL-PERP + amm_const_datum(2, 2, PERCENTAGE_PRECISION_I64, slot), // ETH-PERP + amm_const_datum(3, 0, 46 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for BTC + amm_const_datum(3, 1, 132 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for SOL + amm_const_datum(3, 2, 35 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for ETH + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 6, + ..AmmConstituentMappingFixed::default() + }); + const LEN: usize = 6; + const DATA_SIZE: usize = std::mem::size_of::() * LEN; + let defaults: [AmmConstituentDatum; LEN] = [AmmConstituentDatum::default(); LEN]; + let mapping_data = RefCell::new(unsafe { + std::mem::transmute::<[AmmConstituentDatum; LEN], [u8; DATA_SIZE]>(defaults) + }); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in amm_data { + println!("Adding AMM Constituent Datum: {:?}", amm_datum); + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_price: Vec = vec![ + AmmInventoryAndPrices { + inventory: 4 * BASE_PRECISION_I64, + price: 100_000 * PRICE_PRECISION_I64, + }, // $400k BTC + AmmInventoryAndPrices { + inventory: 2000 * BASE_PRECISION_I64, + price: 200 * PRICE_PRECISION_I64, + }, // $400k SOL + AmmInventoryAndPrices { + inventory: 200 * BASE_PRECISION_I64, + price: 1500 * PRICE_PRECISION_I64, + }, // $300k ETH + AmmInventoryAndPrices { + inventory: 16500 * BASE_PRECISION_I64, + price: PRICE_PRECISION_I64, + }, // $16.5k FARTCOIN + ]; + let mut constituents_indexes_and_decimals_and_prices = vec![ + ConstituentIndexAndDecimalAndPrice { + constituent_index: 0, + decimals: 6, + price: 100_000 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 200 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 2, + decimals: 6, + price: 1500 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 3, + decimals: 6, + price: PRICE_PRECISION_I64, + }, // USDC + ]; + let aum = 2_000_000 * QUOTE_PRECISION; // $2M AUM + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 96]); + let now_ts = 1234567890; + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_price, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + now_ts, + ) + .unwrap(); + + let target_weights: Vec = target_zc_mut + .iter() + .enumerate() + .map(|(index, datum)| { + calculate_target_weight( + datum.target_base.cast::().unwrap(), + &SpotMarket::default_quote_market(), + amm_inventory_and_price.get(index).unwrap().price, + aum, + ) + .unwrap() + }) + .collect(); + + println!("Target Weights: {:?}", target_weights); + assert_eq!(target_weights.len(), 4); + assert_eq!(target_weights[0], -203795); // 20.3% BTC + assert_eq!(target_weights[1], -210890); // 21.1% SOL + assert_eq!(target_weights[2], -152887); // 15.3% ETH + assert_eq!(target_weights[3], 0); // USDC not set if it's not in AUM update + } + + #[test] + fn test_single_zero_weight() { + let amm_datum = amm_const_datum(0, 1, 0, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { + inventory: 1_000_000, + price: 1_000_000, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 1_000_000, + }]; + let now_ts = 1000; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 24]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + now_ts, + ) + .unwrap(); + + assert!(target_zc_mut.iter().all(|&x| x.target_base == 0)); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, 0); + assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + } + + #[test] + fn test_single_full_weight() { + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let price = PRICE_PRECISION_I64; + let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { + inventory: BASE_PRECISION_I64, + price, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price, + }]; + let aum = 1_000_000; + let now_ts = 1234; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 24]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + now_ts, + ) + .unwrap(); + + let weight = calculate_target_weight( + target_zc_mut.get(0).target_base as i64, + &SpotMarket::default(), + price, + aum, + ) + .unwrap(); + + assert_eq!( + target_zc_mut.get(0).target_base as i128, + -1 * 10_i128.pow(6_u32) + ); + assert_eq!(weight, -1000000); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + } + + #[test] + fn test_multiple_constituents_partial_weights() { + let amm_mapping_data = vec![ + amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64 / 2, 111), + amm_const_datum(0, 2, PERCENTAGE_PRECISION_I64 / 2, 111), + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: amm_mapping_data.len() as u32, + ..AmmConstituentMappingFixed::default() + }); + + // 48 = size_of::() * amm_mapping_data.len() + let mapping_data = RefCell::new([0u8; 48]); + + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in &amm_mapping_data { + mapping_zc_mut + .add_amm_constituent_datum(*amm_datum) + .unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { + inventory: 1_000_000_000, + price: 1_000_000, + }]; + let mut constituents_indexes_and_decimals_and_prices = vec![ + ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 1_000_000, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 2, + decimals: 6, + price: 1_000_000, + }, + ]; + + let aum = 1_000_000; + let now_ts = 999; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: amm_mapping_data.len() as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 48]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + now_ts, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 2); + + for i in 0..target_zc_mut.len() { + assert_eq!( + calculate_target_weight( + target_zc_mut.get(i).target_base, + &SpotMarket::default_quote_market(), + constituents_indexes_and_decimals_and_prices + .get(i as usize) + .unwrap() + .price, + aum, + ) + .unwrap(), + -1 * PERCENTAGE_PRECISION_I64 / 2 + ); + assert_eq!(target_zc_mut.get(i).last_slot, now_ts); + } + } + + #[test] + fn test_zero_aum_safe() { + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { + inventory: 1_000_000, + price: 142_000_000, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 142_000_000, + }]; + + let now_ts = 111; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 24]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + now_ts, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, -1_000); // despite no aum, desire to reach target + assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + } +} + +#[cfg(test)] +mod swap_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I128, PRICE_PRECISION_I64, + SPOT_BALANCE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_swap_price() { + let lp_pool = LPPool::default(); + + let in_oracle = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let out_oracle = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + // same decimals + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &in_oracle, &out_oracle) + .unwrap(); + assert_eq!(price_num, 1_000_000); + assert_eq!(price_denom, 233_400_000); + + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &out_oracle, &in_oracle) + .unwrap(); + assert_eq!(price_num, 233_400_000); + assert_eq!(price_denom, 1_000_000); + } + + fn get_swap_amount_decimals_scenario( + in_current_weight: u64, + out_current_weight: u64, + in_decimals: u32, + out_decimals: u32, + in_amount: u64, + expected_in_amount: u128, + expected_out_amount: u128, + expected_in_fee: i128, + expected_out_fee: i128, + in_xi: u8, + out_xi: u8, + in_gamma_inventory: u8, + out_gamma_inventory: u8, + in_gamma_execution: u8, + out_gamma_execution: u8, + in_volatility: u64, + out_volatility: u64, + ) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let in_token_amount = in_notional * 10_u128.pow(in_decimals) / oracle_0.price as u128; + + let out_notional = (out_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let out_token_amount = out_notional * 10_u128.pow(out_decimals) / oracle_1.price as u128; + + let constituent_0 = Constituent { + decimals: in_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: in_gamma_execution, + gamma_inventory: in_gamma_inventory, + xi: in_xi, + volatility: in_volatility, + vault_token_balance: in_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: out_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: out_gamma_execution, + gamma_inventory: out_gamma_inventory, + xi: out_xi, + volatility: out_volatility, + vault_token_balance: out_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: out_decimals, + ..SpotMarket::default() + }; + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + 500_000, + 500_000, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + assert_eq!(in_amount, expected_in_amount); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(in_fee, expected_in_fee); + assert_eq!(out_fee, expected_out_fee); + } + + #[test] + fn test_get_swap_amount_in_6_out_6() { + get_swap_amount_decimals_scenario( + 500_000, + 500_000, + 6, + 6, + 150_000_000_000, + 150_000_000_000, + 642577120, + 22500000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_6_out_9() { + get_swap_amount_decimals_scenario( + 500_000, + 500_000, + 6, + 9, + 150_000_000_000, + 150_000_000_000, + 642577120822, + 22500000, + 282091356, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_9_out_6() { + get_swap_amount_decimals_scenario( + 500_000, + 500_000, + 9, + 6, + 150_000_000_000_000, + 150_000_000_000_000, + 642577120, + 22500000000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_weight() { + let c = Constituent { + swap_fee_min: -1 * PERCENTAGE_PRECISION_I64 / 10000, // -1 bps (rebate) + swap_fee_max: PERCENTAGE_PRECISION_I64 / 100, // 100 bps + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, // 10% + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 500_000, + cumulative_deposits: 1_000_000, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: 500_000, + decimals: 6, + ..Constituent::default() + }; + + let spot_market = SpotMarket { + market_index: 0, + decimals: 6, + cumulative_deposit_interest: 10_000_000_000_000, + ..SpotMarket::default() + }; + + let full_balance = c.get_full_token_amount(&spot_market).unwrap(); + assert_eq!(full_balance, 1_000_000); + + // 1/10 = 10% + let weight = c + .get_weight( + 1_000_000, // $1 + &spot_market, + 0, + 10_000_000, + ) + .unwrap(); + assert_eq!(weight, 100_000); + + // (1+1)/10 = 20% + let weight = c + .get_weight(1_000_000, &spot_market, 1_000_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 200_000); + + // (1-0.5)/10 = 0.5% + let weight = c + .get_weight(1_000_000, &spot_market, -500_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 50_000); + } + + fn get_add_liquidity_mint_amount_scenario( + last_aum: u128, + now: i64, + in_decimals: u32, + in_amount: u128, + dlp_total_supply: u64, + expected_lp_amount: u64, + expected_lp_fee: i64, + expected_in_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + last_hedge_ts: 0, + min_mint_fee: 0, + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount, in_amount_1, lp_fee, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + &spot_market, + &constituent, + in_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount, expected_lp_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(in_amount_1, in_amount); + assert_eq!(in_fee_amount, expected_in_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_add_liquidity_mint_amount_zero_aum() { + get_add_liquidity_mint_amount_scenario( + 0, // last_aum + 0, // now + 6, // in_decimals + 1_000_000, // in_amount + 0, // dlp_total_supply (non-zero to avoid MathError) + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum() { + get_add_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 300, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, // last_aum + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount in lp decimals + 0, // expected_lp_fee + 30000, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 4 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, // last_aum + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1000000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 3, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + fn get_remove_liquidity_mint_amount_scenario( + last_aum: u128, + now: i64, + in_decimals: u32, + lp_burn_amount: u64, + dlp_total_supply: u64, + expected_out_amount: u128, + expected_lp_fee: i64, + expected_out_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + last_hedge_ts: 0, + min_mint_fee: 100, // 1 bps + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount_1, out_amount, lp_fee, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + &spot_market, + &constituent, + lp_burn_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount_1, lp_burn_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(out_fee_amount, expected_out_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999900, // expected_out_amount + 100, // expected_lp_fee + 299, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 9999000000, // expected_out_amount + 10000, // expected_lp_fee + 2999700, + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 4 decimal constituent + // there will be a problem with 4 decimal constituents with aum ~10M + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = 1/10000 + 10_000_000_000, // dlp_total_supply + 99, // expected_out_amount + 1, // expected_lp_fee + 0, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_5_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 5, // in_decimals + 100_000_000_000 * 100_000, // in_amount + 100_000_000_000 * 1_000_000, // dlp_total_supply + 999900000000000, // expected_out_amount + 1000000000000, // expected_lp_fee + 473952600000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_6_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 6, // in_decimals + 100_000_000_000 * 1_000_000, // in_amount + 100_000_000_000 * 1_000_000, // dlp_total_supply + 99990000000000000, // expected_out_amount + 10000000000000, // expected_lp_fee + 349765020000000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000_000_000, // last_aum ($10,000,000,000) + 0, // now + 8, // in_decimals + 10_000_000_000 * 100_000_000, // in_amount + 10_000_000_000 * 1_000_000, // dlp_total_supply + 9_999_000_000_000_000_0000, // expected_out_amount + 100000000000000, // expected_lp_fee + 3764623500000000000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + fn round_to_sig(x: i128, sig: u32) -> i128 { + if x == 0 { + return 0; + } + let digits = (x.abs() as f64).log10().floor() as u32 + 1; + let factor = 10_i128.pow(digits - sig); + ((x + factor / 2) / factor) * factor + } + + fn get_swap_amounts( + in_oracle_price: i64, + out_oracle_price: i64, + in_current_weight: i64, + out_current_weight: i64, + in_amount: u64, + in_volatility: u64, + out_volatility: u64, + in_target_weight: i64, + out_target_weight: i64, + ) -> (u128, u128, i128, i128, i128, i128) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: in_oracle_price, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: out_oracle_price, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let in_token_amount = in_notional * 10_i128.pow(6) / oracle_0.price as i128; + let in_spot_balance = if in_token_amount > 0 { + ConstituentSpotBalance { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + } else { + ConstituentSpotBalance { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Borrow, + market_index: 0, + ..ConstituentSpotBalance::default() + } + }; + + let out_notional = (out_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let out_token_amount = out_notional * 10_i128.pow(6) / oracle_1.price as i128; + let out_spot_balance = if out_token_amount > 0 { + ConstituentSpotBalance { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + } else { + ConstituentSpotBalance { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + }; + + let constituent_0 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 1, + gamma_inventory: 1, + xi: 1, + volatility: in_volatility, + spot_balance: in_spot_balance, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 2, + gamma_inventory: 2, + xi: 2, + volatility: out_volatility, + spot_balance: out_spot_balance, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + + let (in_amount_result, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + in_target_weight, + out_target_weight, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + + return ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount, + out_token_amount, + ); + } + + #[test] + fn grid_search_swap() { + let weights: [i64; 20] = [ + -100_000, -200_000, -300_000, -400_000, -500_000, -600_000, -700_000, -800_000, + -900_000, -1_000_000, 100_000, 200_000, 300_000, 400_000, 500_000, 600_000, 700_000, + 800_000, 900_000, 1_000_000, + ]; + let in_amounts: Vec = (0..=10) + .map(|i| (1000 + i * 20000) * 10_u64.pow(6)) + .collect(); + + let volatilities: Vec = (1..=10) + .map(|i| PERCENTAGE_PRECISION_U64 * i / 100) + .collect(); + + let in_oracle_price = PRICE_PRECISION_I64; // $1 + let out_oracle_price = 233_400_000; // $233.4 + + // Assert monotonically increasing fees in in_amounts + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + for out_volatility in volatilities.iter() { + let mut prev_in_fee_bps = 0_i128; + let mut prev_out_fee_bps = 0_i128; + for in_amount in in_amounts.iter() { + let ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount_pre, + out_token_amount_pre, + ) = get_swap_amounts( + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + 0, + *out_volatility, + PERCENTAGE_PRECISION_I64, // 100% target weight + PERCENTAGE_PRECISION_I64, // 100% target weight + ); + + // Calculate fee in basis points with precision + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + // Assert monotonically increasing fees + if in_amounts.iter().position(|&x| x == *in_amount).unwrap() > 0 { + assert!( + in_fee_bps >= prev_in_fee_bps, + "in_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_amount, + out_volatility + ); + assert!( + out_fee_bps >= prev_out_fee_bps, + "out_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + in_amount, + out_volatility + ); + } + + println!( + "in_weight: {}, out_weight: {}, in_amount: {}, out_amount: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_amount_result, + out_amount, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + + prev_in_fee_bps = in_fee_bps; + prev_out_fee_bps = out_fee_bps; + } + } + } + + // Assert monotonically increasing fees based on error improvement + for in_amount in in_amounts.iter() { + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + let fixed_volatility = PERCENTAGE_PRECISION_U64 * 5 / 100; + let target_weights: Vec = (1..=20).map(|i| i * 50_000).collect(); + + let mut results: Vec<(i128, i128, i128, i128, i128, i128)> = Vec::new(); + + for target_weight in target_weights.iter() { + let in_target_weight = *target_weight; + let out_target_weight = 1_000_000 - in_target_weight; + + let ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount_pre, + out_token_amount_pre, + ) = get_swap_amounts( + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + fixed_volatility, + fixed_volatility, + in_target_weight, + out_target_weight, + ); + + // Calculate weights after swap + + let out_token_after = out_token_amount_pre - out_amount as i128 + out_fee; + let in_token_after = in_token_amount_pre + in_amount_result as i128; + + let out_notional_after = + out_token_after * (out_oracle_price as i128) / PRICE_PRECISION_I128; + let in_notional_after = + in_token_after * (in_oracle_price as i128) / PRICE_PRECISION_I128; + let total_notional_after = in_notional_after + out_notional_after; + + let out_weight_after = + (out_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + let in_weight_after = + (in_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + + // Calculate error improvement (positive means improvement) + let in_error_before = (*in_current_weight - in_target_weight).abs() as i128; + let out_error_before = (out_current_weight - out_target_weight).abs() as i128; + + let in_error_after = (in_weight_after - in_target_weight as i128).abs(); + let out_error_after = (out_weight_after - out_target_weight as i128).abs(); + + let in_error_improvement = round_to_sig(in_error_before - in_error_after, 2); + let out_error_improvement = round_to_sig(out_error_before - out_error_after, 2); + + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + results.push(( + in_error_improvement, + out_error_improvement, + in_fee_bps, + out_fee_bps, + in_target_weight as i128, + out_target_weight as i128, + )); + + println!( + "in_weight: {}, out_weight: {}, in_target: {}, out_target: {}, in_error_improvement: {}, out_error_improvement: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_target_weight, + out_target_weight, + in_error_improvement, + out_error_improvement, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + } + + // Sort by in_error_improvement and check monotonicity + results.sort_by_key(|&(in_error_improvement, _, _, _, _, _)| -in_error_improvement); + + for i in 1..results.len() { + let (prev_in_improvement, _, prev_in_fee_bps, _, _, _) = results[i - 1]; + let (curr_in_improvement, _, curr_in_fee_bps, _, in_target, _) = results[i]; + + // Less improvement should mean higher fees + if curr_in_improvement < prev_in_improvement { + assert!( + curr_in_fee_bps >= prev_in_fee_bps, + "in_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, in_weight: {}, in_target: {}", + curr_in_improvement, + prev_in_improvement, + curr_in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_target + ); + } + } + + // Sort by out_error_improvement and check monotonicity + results + .sort_by_key(|&(_, out_error_improvement, _, _, _, _)| -out_error_improvement); + + for i in 1..results.len() { + let (_, prev_out_improvement, _, prev_out_fee_bps, _, _) = results[i - 1]; + let (_, curr_out_improvement, _, curr_out_fee_bps, _, out_target) = results[i]; + + // Less improvement should mean higher fees + if curr_out_improvement < prev_out_improvement { + assert!( + curr_out_fee_bps >= prev_out_fee_bps, + "out_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, out_weight: {}, out_target: {}", + curr_out_improvement, + prev_out_improvement, + curr_out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + out_target + ); + } + } + } + } + } +} + +#[cfg(test)] +mod swap_fee_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_gamma_covar_matrix() { + // in = sol, out = btc + let covar_matrix = get_gamma_covar_matrix( + PERCENTAGE_PRECISION_I64, + 2, // gamma sol + 2, // gamma btc + 4 * PERCENTAGE_PRECISION_U64 / 100, // vol sol + 3 * PERCENTAGE_PRECISION_U64 / 100, // vol btc + ) + .unwrap(); + assert_eq!(covar_matrix, [[3200, 2400], [2400, 1800]]); + } + + #[test] + fn test_lp_pool_get_linear_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let trade_ratio = 5_000_000 * QUOTE_PRECISION_I128 * PERCENTAGE_PRECISION_I128 + / (15_000_000 * QUOTE_PRECISION_I128); + + let fee_execution_linear = lp_pool + .get_linear_fee_execution( + trade_ratio, + 1600, // 0.0016 + 2, + ) + .unwrap(); + + assert_eq!(fee_execution_linear, 1066); // 10.667 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let trade_ratio = 5_000_000 * QUOTE_PRECISION_I128 * PERCENTAGE_PRECISION_I128 + / (15_000_000 * QUOTE_PRECISION_I128); + + let fee_execution_quadratic = lp_pool + .get_quadratic_fee_execution( + trade_ratio, + 1600, // 0.0016 + 2, + ) + .unwrap(); + + assert_eq!(fee_execution_quadratic, 711); // 7.1 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_inventory() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let (fee_in, fee_out) = lp_pool + .get_quadratic_fee_inventory( + [[3200, 2400], [2400, 1800]], + [ + 1_000_000 * QUOTE_PRECISION_I128, + -500_000 * QUOTE_PRECISION_I128, + ], + [ + -4_000_000 * QUOTE_PRECISION_I128, + 4_500_000 * QUOTE_PRECISION_I128, + ], + 5_000_000 * QUOTE_PRECISION, + ) + .unwrap(); + + assert_eq!(fee_in, 6 * PERCENTAGE_PRECISION_I128 / 100000); // 0.6 bps + assert_eq!(fee_out, -6 * PERCENTAGE_PRECISION_I128 / 100000); // -0.6 bps + } +} + +#[cfg(test)] +mod settle_tests { + use crate::math::lp_pool::perp_lp_pool_settlement::{ + calculate_settlement_amount, update_cache_info, SettlementContext, SettlementDirection, + SettlementResult, + }; + use crate::state::amm_cache::CacheInfo; + use crate::state::spot_market::SpotMarket; + + fn create_mock_spot_market() -> SpotMarket { + SpotMarket::default() + } + + #[test] + fn test_calculate_settlement_no_amount_owed() { + let ctx = SettlementContext { + quote_owed_from_lp: 0, + quote_constituent_token_balance: 1000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_lp_to_perp_settlement_sufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_lp_to_perp_settlement_insufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 1500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1000); // Limited by LP balance + } + + #[test] + fn test_lp_to_perp_settlement_no_lp_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500, + quote_constituent_token_balance: 0, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_perp_to_lp_settlement_fee_pool_sufficient() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 800, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_perp_to_lp_settlement_needs_both_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 700); + } + + #[test] + fn test_perp_to_lp_settlement_insufficient_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1500, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); // Limited by pool balances + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 200); + } + + #[test] + fn test_settlement_edge_cases() { + // Test with zero fee pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 0, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 500); + + // Test with zero pnl pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 300); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_update_cache_info_to_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: -400, + last_fee_pool_token_amount: 2_000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 200, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 120, + pnl_pool_used: 80, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + result.amount_transferred as i64; + let ts = 99; + let slot = 100; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 200); + assert_eq!(cache.last_settle_slot, slot); + // fee pool decreases by fee_pool_used + assert_eq!(cache.last_fee_pool_token_amount, 2_000 - 120); + // pnl pool decreases by pnl_pool_used + assert_eq!(cache.last_net_pnl_pool_token_amount, 500 - 80); + } + + #[test] + fn test_update_cache_info_from_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 500, + last_fee_pool_token_amount: 1_000, + last_net_pnl_pool_token_amount: 200, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 150, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - result.amount_transferred as i64; + let ts = 42; + let slot = 100; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 150); + assert_eq!(cache.last_settle_slot, slot); + // fee pool increases by amount_transferred + assert_eq!(cache.last_fee_pool_token_amount, 1_000 + 150); + // pnl pool untouched + assert_eq!(cache.last_net_pnl_pool_token_amount, 200); + } + + #[test] + fn test_large_settlement_amounts() { + // Test with very large amounts to check for overflow + let ctx = SettlementContext { + quote_owed_from_lp: i64::MAX / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_negative_large_settlement_amounts() { + let ctx = SettlementContext { + quote_owed_from_lp: i64::MIN / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_exact_boundary_settlements() { + // Test when quote_owed exactly equals LP balance + let ctx = SettlementContext { + quote_owed_from_lp: 1000, + quote_constituent_token_balance: 1000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1000); + + // Test when negative quote_owed exactly equals total pool balance + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 2000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 300); + } + + #[test] + fn test_minimal_settlement_amounts() { + // Test with minimal positive amount + let ctx = SettlementContext { + quote_owed_from_lp: 1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 1, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1); + + // Test with minimal negative amount + let ctx = SettlementContext { + quote_owed_from_lp: -1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1); + assert_eq!(result.fee_pool_used, 1); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_all_zero_balances() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 0, + fee_pool_balance: 0, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 0); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_cache_info_update_none_direction() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 100, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 50, + last_settle_slot: 12345, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = 100; // No change + let ts = 67890; + let slot = 100000000; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed unchanged + assert_eq!(cache.quote_owed_from_lp_pool, 100); + // settle fields updated with new timestamp but zero amount + assert_eq!(cache.last_settle_amount, 0); + assert_eq!(cache.last_settle_slot, slot); + // pool amounts unchanged + assert_eq!(cache.last_fee_pool_token_amount, 1000); + assert_eq!(cache.last_net_pnl_pool_token_amount, 500); + } + + #[test] + fn test_cache_info_update_maximum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MAX / 2, + last_fee_pool_token_amount: u128::MAX / 2, + last_net_pnl_pool_token_amount: i128::MAX / 2, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: u64::MAX / 4, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - (result.amount_transferred as i64); + let slot = u64::MAX / 2; + let ts = i64::MAX / 2; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, slot, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_cache_info_update_minimum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MIN / 2, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: i128::MIN / 2, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 500, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 200, + pnl_pool_used: 300, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + (result.amount_transferred as i64); + let slot = u64::MAX / 2; + let ts = 42; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, slot, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_sequential_settlement_updates() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 1000, + last_fee_pool_token_amount: 5000, + last_net_pnl_pool_token_amount: 3000, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + // First settlement: From LP pool + let result1 = SettlementResult { + amount_transferred: 300, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed1 = cache.quote_owed_from_lp_pool - (result1.amount_transferred as i64); + update_cache_info(&mut cache, &result1, new_quote_owed1, 101010101, 100).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 700); + assert_eq!(cache.last_fee_pool_token_amount, 5300); + assert_eq!(cache.last_net_pnl_pool_token_amount, 3000); + + // Second settlement: To LP pool + let result2 = SettlementResult { + amount_transferred: 400, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 250, + pnl_pool_used: 150, + }; + let new_quote_owed2 = cache.quote_owed_from_lp_pool + (result2.amount_transferred as i64); + update_cache_info(&mut cache, &result2, new_quote_owed2, 10101010, 200).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 1100); + assert_eq!(cache.last_fee_pool_token_amount, 5050); + assert_eq!(cache.last_net_pnl_pool_token_amount, 2850); + assert_eq!(cache.last_settle_slot, 10101010); + } + + #[test] + fn test_perp_to_lp_with_only_pnl_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 0, // No fee pool + pnl_pool_balance: 1200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 1000); + } + + #[test] + fn test_perp_to_lp_capped_with_max() { + let ctx = SettlementContext { + quote_owed_from_lp: -1100, + quote_constituent_token_balance: 2000, + fee_pool_balance: 500, // No fee pool + pnl_pool_balance: 700, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 1000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 500); + } + + #[test] + fn test_lp_to_perp_capped_with_max() { + let ctx = SettlementContext { + quote_owed_from_lp: 1100, + quote_constituent_token_balance: 2000, + fee_pool_balance: 0, // No fee pool + pnl_pool_balance: 1200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 1000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_perp_to_lp_with_only_fee_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 1500, + fee_pool_balance: 1000, + pnl_pool_balance: 0, // No PnL pool + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 800); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_fractional_settlement_coverage() { + // Test when pools can only partially cover the needed amount + let ctx = SettlementContext { + quote_owed_from_lp: -2000, + quote_constituent_token_balance: 5000, + fee_pool_balance: 300, + pnl_pool_balance: 500, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); // Only what pools can provide + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 500); + } + + #[test] + fn test_settlement_direction_consistency() { + // Positive quote_owed should always result in FromLpPool or None + for quote_owed in [1, 100, 1000, 10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::FromLpPool + || result.direction == SettlementDirection::None + ); + } + + // Negative quote_owed should always result in ToLpPool or None + for quote_owed in [-1, -100, -1000, -10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::ToLpPool + || result.direction == SettlementDirection::None + ); + } + } + + #[test] + fn test_cache_info_timestamp_progression() { + let mut cache = CacheInfo::default(); + + let timestamps = [1000, 2000, 3000, 1500, 5000]; // Including out-of-order + + for (_, &ts) in timestamps.iter().enumerate() { + let result = SettlementResult { + amount_transferred: 100, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + + update_cache_info(&mut cache, &result, 0, 1010101, ts).unwrap(); + assert_eq!(cache.last_settle_ts, ts); + assert_eq!(cache.last_settle_amount, 100); + } + } + + #[test] + fn test_settlement_amount_conservation() { + // Test that fee_pool_used + pnl_pool_used = amount_transferred for ToLpPool + let test_cases = [ + (-500, 1000, 300, 400), // Normal case + (-1000, 2000, 600, 500), // Uses both pools + (-200, 500, 0, 300), // Only PnL pool + (-150, 400, 200, 0), // Only fee pool + ]; + + for (quote_owed, lp_balance, fee_pool, pnl_pool) in test_cases { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: lp_balance, + fee_pool_balance: fee_pool, + pnl_pool_balance: pnl_pool, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + + if result.direction == SettlementDirection::ToLpPool { + assert_eq!( + result.amount_transferred as u128, + result.fee_pool_used + result.pnl_pool_used, + "Amount transferred should equal sum of pool usage for case: {:?}", + (quote_owed, lp_balance, fee_pool, pnl_pool) + ); + } + } + } + + #[test] + fn test_cache_pool_balance_tracking() { + let mut cache = CacheInfo { + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + ..Default::default() + }; + + // Multiple settlements that should maintain balance consistency + let settlements = [ + (SettlementDirection::ToLpPool, 200, 120, 80), // Uses both pools + (SettlementDirection::FromLpPool, 150, 0, 0), // Adds to fee pool + (SettlementDirection::ToLpPool, 100, 100, 0), // Uses only fee pool + (SettlementDirection::ToLpPool, 50, 30, 20), // Uses both pools again + ]; + + let mut expected_fee_pool = cache.last_fee_pool_token_amount; + let mut expected_pnl_pool = cache.last_net_pnl_pool_token_amount; + + for (direction, amount, fee_used, pnl_used) in settlements { + let result = SettlementResult { + amount_transferred: amount, + direction, + fee_pool_used: fee_used, + pnl_pool_used: pnl_used, + }; + + match direction { + SettlementDirection::FromLpPool => { + expected_fee_pool += amount as u128; + } + SettlementDirection::ToLpPool => { + expected_fee_pool -= fee_used; + expected_pnl_pool -= pnl_used as i128; + } + SettlementDirection::None => {} + } + + update_cache_info(&mut cache, &result, 0, 1000, 0).unwrap(); + + assert_eq!(cache.last_fee_pool_token_amount, expected_fee_pool); + assert_eq!(cache.last_net_pnl_pool_token_amount, expected_pnl_pool); + } + } +} + +#[cfg(test)] +mod update_aum_tests { + use crate::{ + create_anchor_account_info, + math::constants::SPOT_CUMULATIVE_INTEREST_PRECISION, + math::constants::{PRICE_PRECISION_I64, QUOTE_PRECISION}, + state::amm_cache::{AmmCacheFixed, CacheInfo}, + state::lp_pool::*, + state::oracle::HistoricalOracleData, + state::oracle::OracleSource, + state::spot_market::SpotMarket, + state::spot_market_map::SpotMarketMap, + state::zero_copy::AccountZeroCopyMut, + test_utils::{create_account_info, get_anchor_account_bytes}, + }; + use anchor_lang::prelude::Pubkey; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_aum_with_balances( + usdc_balance: u64, // USDC balance in tokens (6 decimals) + sol_balance: u64, // SOL balance in tokens (9 decimals) + btc_balance: u64, // BTC balance in tokens (8 decimals) + bonk_balance: u64, // BONK balance in tokens (5 decimals) + expected_aum_usd: u64, + test_name: &str, + ) { + let mut lp_pool = LPPool::default(); + lp_pool.constituents = 4; + lp_pool.quote_consituent_index = 0; + + // Create constituents with specified token balances + let mut constituent_usdc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 0, + constituent_index: 0, + last_oracle_price: PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 6, + vault_token_balance: usdc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_usdc, Constituent, constituent_usdc_account_info); + + let mut constituent_sol = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 1, + constituent_index: 1, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + vault_token_balance: sol_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_sol, Constituent, constituent_sol_account_info); + + let mut constituent_btc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 2, + constituent_index: 2, + last_oracle_price: 100_000 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 8, + vault_token_balance: btc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_btc, Constituent, constituent_btc_account_info); + + let mut constituent_bonk = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 3, + constituent_index: 3, + last_oracle_price: 22, // $0.000022 in PRICE_PRECISION_I64 + last_oracle_slot: 100, + decimals: 5, + vault_token_balance: bonk_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_bonk, Constituent, constituent_bonk_account_info); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &constituent_usdc_account_info, + &constituent_sol_account_info, + &constituent_btc_account_info, + &constituent_bonk_account_info, + ], + true, + ) + .unwrap(); + + // Create spot markets + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + 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, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + + let mut btc_spot_market = SpotMarket { + market_index: 2, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 8, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!(btc_spot_market, SpotMarket, btc_spot_market_account_info); + + let mut bonk_spot_market = SpotMarket { + market_index: 3, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 5, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!(bonk_spot_market, SpotMarket, bonk_spot_market_account_info); + + let spot_market_account_infos = vec![ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + &btc_spot_market_account_info, + &bonk_spot_market_account_info, + ]; + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 96]); // 4 * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Create AMM cache + let mut cache_fixed_default = AmmCacheFixed::default(); + cache_fixed_default.len = 0; // No perp markets for this test + let cache_fixed = RefCell::new(cache_fixed_default); + let cache_data = RefCell::new([0u8; 0]); // Empty cache data + let amm_cache = AccountZeroCopyMut::<'_, CacheInfo, AmmCacheFixed> { + fixed: cache_fixed.borrow_mut(), + data: cache_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Call update_aum + let result = lp_pool.update_aum( + 101, // slot + &constituent_map, + &spot_market_map, + &constituent_target_base, + &amm_cache, + ); + + assert!(result.is_ok(), "{}: update_aum should succeed", test_name); + let (aum, crypto_delta, derivative_groups) = result.unwrap(); + + // Convert expected USD to quote precision + let expected_aum = expected_aum_usd as u128 * QUOTE_PRECISION; + + println!( + "{}: AUM = ${}, Expected = ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION + ); + + // Verify the results (allow small rounding differences) + let aum_diff = if aum > expected_aum { + aum - expected_aum + } else { + expected_aum - aum + }; + assert!( + aum_diff <= QUOTE_PRECISION, // Allow up to $1 difference for rounding + "{}: AUM mismatch. Got: ${}, Expected: ${}, Diff: ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION, + aum_diff / QUOTE_PRECISION + ); + + assert_eq!(crypto_delta, 0, "{}: crypto_delta should be 0", test_name); + assert!( + derivative_groups.is_empty(), + "{}: derivative_groups should be empty", + test_name + ); + + // Verify LP pool state was updated + assert_eq!( + lp_pool.last_aum, aum, + "{}: last_aum should match calculated AUM", + test_name + ); + assert_eq!( + lp_pool.last_aum_slot, 101, + "{}: last_aum_slot should be updated", + test_name + ); + } + + #[test] + fn test_aum_zero() { + test_aum_with_balances( + 0, // 0 USDC + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 0, // $0 expected AUM + "Zero AUM", + ); + } + + #[test] + fn test_aum_low_1k() { + test_aum_with_balances( + 1_000_000_000, // 1,000 USDC (6 decimals) = $1,000 + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 1_000, // $1,000 expected AUM + "Low AUM (~$1k)", + ); + } + + #[test] + fn test_aum_reasonable() { + test_aum_with_balances( + 1_000_000_000_000, // 1M USDC (6 decimals) = $1M + 5_000_000_000_000, // 5k SOL (9 decimals) = $1M at $200/SOL + 800_000_000, // 8 BTC (8 decimals) = $800k at $100k/BTC + 0, // 0 BONK + 2_800_000, // Expected AUM based on actual calculation + "Reasonable AUM (~$2.8M)", + ); + } + + #[test] + fn test_aum_high() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 0, // 0 BONK + 210_000_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b)", + ); + } + + #[test] + fn test_aum_with_small_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000, // 1B BONK (5 decimals) = $22k at $0.000022/BONK + 210_000_022_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } + + #[test] + fn test_aum_with_large_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000_000, // 1T BONK (5 decimals) = $22M at $0.000022/BONK + 210_022_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } +} + +#[cfg(test)] +mod update_constituent_target_base_for_derivatives_tests { + use super::super::update_constituent_target_base_for_derivatives; + use crate::create_anchor_account_info; + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, QUOTE_PRECISION, + SPOT_CUMULATIVE_INTEREST_PRECISION, + }; + use crate::state::constituent_map::ConstituentMap; + use crate::state::lp_pool::{Constituent, ConstituentTargetBaseFixed, TargetsDatum}; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::spot_market::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::zero_copy::AccountZeroCopyMut; + use crate::test_utils::{create_account_info, get_anchor_account_bytes}; + use anchor_lang::prelude::Pubkey; + use anchor_lang::Owner; + use std::collections::BTreeMap; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_derivative_weights_scenario( + derivative_weights: Vec, + test_name: &str, + should_succeed: bool, + ) { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, // Parent doesn't have derivative weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create first derivative constituent + let derivative1_index = parent_index + 1; // 2 + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, // $195 (slightly below parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(0).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Create second derivative constituent + let derivative2_index = parent_index + 2; // 3 + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 205 * PRICE_PRECISION_I64, // $205 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(1).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + // Create third derivative constituent + let derivative3_index = parent_index + 3; // 4 + let mut derivative3_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative3_index, + constituent_index: derivative3_index, + last_oracle_price: 210 * PRICE_PRECISION_I64, // $210 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(2).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative3_constituent, + Constituent, + derivative3_constituent_account_info + ); + + let constituents_list = vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + &derivative3_constituent_account_info, + ]; + let constituent_map = ConstituentMap::load_multiple(constituents_list, true).unwrap(); + + // Create spot markets + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let mut derivative3_spot_market = SpotMarket { + market_index: derivative3_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative3_spot_market, + SpotMarket, + derivative3_spot_market_account_info + ); + + let spot_market_list = vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + &derivative3_spot_market_account_info, + ]; + let spot_market_map = SpotMarketMap::load_multiple(spot_market_list, true).unwrap(); + + // Create constituent target base + let num_constituents = 4; // Fixed: parent + 3 derivatives + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: num_constituents as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 120]); // 4+1 constituents * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial parent target base (targeting 10% of total AUM worth of SOL tokens) + // For 10M AUM and $200 SOL price with 9 decimals: (10M * 0.1) / 200 * 10^9 = 5,000,000,000,000 tokens + let initial_parent_target_base = 5_000_000_000_000i64; // ~$1M worth of SOL tokens + constituent_target_base + .get_mut(parent_index as u32) + .target_base = initial_parent_target_base; + constituent_target_base + .get_mut(parent_index as u32) + .last_slot = 100; + + // Initialize derivative target bases to 0 + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative1_index as u32) + .last_slot = 100; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative2_index as u32) + .last_slot = 100; + constituent_target_base + .get_mut(derivative3_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative3_index as u32) + .last_slot = 100; + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + let mut active_derivatives = Vec::new(); + for (i, _) in derivative_weights.iter().enumerate() { + // Add all derivatives regardless of weight (they may have zero weight for testing) + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + active_derivatives.push(derivative_index); + } + if !active_derivatives.is_empty() { + derivative_groups.insert(parent_index, active_derivatives); + } + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok() == should_succeed, + "{}: update_constituent_target_base_for_derivatives should succeed", + test_name + ); + + if !should_succeed { + return; + } + + // Verify results + let parent_target_base_after = constituent_target_base.get(parent_index as u32).target_base; + let total_derivative_weight: u64 = derivative_weights.iter().sum(); + let remaining_parent_weight = PERCENTAGE_PRECISION_U64 - total_derivative_weight; + + // Expected parent target base after scaling down + let expected_parent_target_base = initial_parent_target_base + * (remaining_parent_weight as i64) + / (PERCENTAGE_PRECISION_I64); + + println!( + "{}: Original parent target base: {}, After: {}, Expected: {}", + test_name, + initial_parent_target_base, + parent_target_base_after, + expected_parent_target_base + ); + + assert_eq!( + parent_target_base_after, expected_parent_target_base, + "{}: Parent target base should be scaled down correctly", + test_name + ); + + // Verify derivative target bases + for (i, derivative_weight) in derivative_weights.iter().enumerate() { + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + + if *derivative_weight == 0 { + // If derivative weight is 0, target base should remain 0 + assert_eq!( + derivative_target_base, 0, + "{}: Derivative {} with zero weight should have target base 0", + test_name, derivative_index + ); + continue; + } + + // For simplicity, just verify that the derivative target base is positive and reasonable + // The exact calculation is complex and depends on the internal implementation + println!( + "{}: Derivative {} target base: {}, Weight: {}", + test_name, derivative_index, derivative_target_base, derivative_weight + ); + + assert!( + derivative_target_base > 0, + "{}: Derivative {} target base should be positive", + test_name, + derivative_index + ); + + // Verify that target base is reasonable (not too large or too small) + assert!( + derivative_target_base < 10_000_000_000_000i64, + "{}: Derivative {} target base should be reasonable", + test_name, + derivative_index + ); + } + } + + fn test_depeg_scenario() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create derivative constituent that's depegged - must have different index than parent + let derivative_index = parent_index + 1; // 2 + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Create spot markets + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 72]); // 2+1 constituents * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial values + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 2_500_000_000_000i64; // ~$500k worth of SOL + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 1_250_000_000_000i64; // ~$250k worth + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "depeg scenario: update_constituent_target_base_for_derivatives should succeed" + ); + + // Verify that depegged derivative has target base set to 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "depeg scenario: Depegged derivative should have target base 0" + ); + + // Verify that parent target base is unchanged since derivative weight is 0 now + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + assert_eq!( + parent_target_base, 2_500_000_000_000i64, + "depeg scenario: Parent target base should remain unchanged" + ); + } + + #[test] + fn test_derivative_depeg_scenario() { + // Test case: Test depeg scenario + test_depeg_scenario(); + } + + #[test] + fn test_derivative_weights_sum_to_110_percent() { + // Test case: Derivative constituents with weights that sum to 1.1 (110%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 300_000, // 30% weight + ], + "weights sum to 110%", + false, + ); + } + + #[test] + fn test_derivative_weights_sum_to_100_percent() { + // Test case: Derivative constituents with weights that sum to 1 (100%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 200_000, // 20% weight + ], + "weights sum to 100%", + true, + ); + } + + #[test] + fn test_derivative_weights_sum_to_75_percent() { + // Test case: Derivative constituents with weights that sum to < 1 (75%) + test_derivative_weights_scenario( + vec![ + 400_000, // 40% weight + 200_000, // 20% weight + 150_000, // 15% weight + ], + "weights sum to 75%", + true, + ); + } + + #[test] + fn test_single_derivative_60_percent_weight() { + // Test case: Single derivative with partial weight + test_derivative_weights_scenario( + vec![ + 600_000, // 60% weight + ], + "single derivative 60% weight", + true, + ); + } + + #[test] + fn test_single_derivative_100_percent_weight() { + // Test case: Single derivative with 100% weight - parent should become 0 + test_derivative_weights_scenario( + vec![ + 1_000_000, // 100% weight + ], + "single derivative 100% weight", + true, + ); + } + + #[test] + fn test_mixed_zero_and_nonzero_weights() { + // Test case: Mix of zero and non-zero weights + test_derivative_weights_scenario( + vec![ + 0, // 0% weight + 400_000, // 40% weight + 0, // 0% weight + ], + "mixed zero and non-zero weights", + true, + ); + } + + #[test] + fn test_very_small_weights() { + // Test case: Very small weights (1 basis point = 0.01%) + test_derivative_weights_scenario( + vec![ + 100, // 0.01% weight + 200, // 0.02% weight + 300, // 0.03% weight + ], + "very small weights", + true, + ); + } + + #[test] + fn test_zero_parent_target_base() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + let derivative_index = parent_index + 1; + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 72]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set parent target base to 0 + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "zero parent target base scenario should succeed" + ); + + // With zero parent target base, derivative should also be 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "zero parent target base: derivative target base should be 0" + ); + } + + #[test] + fn test_mixed_depegged_and_valid_derivatives() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 949_999, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // First derivative - depegged + let derivative1_index = parent_index + 1; + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 300_000, // 30% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Second derivative - valid + let derivative2_index = parent_index + 2; + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 198 * PRICE_PRECISION_I64, // $198 (above 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 400_000, // 40% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + ], + true, + ) + .unwrap(); + + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + ], + true, + ) + .unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 3, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 96]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 5_000_000_000_000i64; + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative1_index, derivative2_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "mixed depegged and valid derivatives scenario should succeed" + ); + + // First derivative should be depegged (target base = 0) + let derivative1_target_base = constituent_target_base + .get(derivative1_index as u32) + .target_base; + assert_eq!( + derivative1_target_base, 0, + "mixed scenario: depegged derivative should have target base 0" + ); + + // Second derivative should have positive target base + let derivative2_target_base = constituent_target_base + .get(derivative2_index as u32) + .target_base; + assert!( + derivative2_target_base > 0, + "mixed scenario: valid derivative should have positive target base" + ); + + // Parent should be scaled down by only the valid derivative's weight (40%) + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + let expected_parent_target_base = 5_000_000_000_000i64 * (1_000_000 - 400_000) / 1_000_000; + assert_eq!( + parent_target_base, expected_parent_target_base, + "mixed scenario: parent should be scaled by valid derivative weight only" + ); + } +} diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index a9c9724757..69ac0eb312 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -1,3 +1,5 @@ +pub mod amm_cache; +pub mod constituent_map; pub mod events; pub mod fill_mode; pub mod fulfillment; @@ -6,6 +8,7 @@ pub mod high_leverage_mode_config; pub mod if_rebalance_config; pub mod insurance_fund_stake; pub mod load_ref; +pub mod lp_pool; pub mod margin_calculation; pub mod oracle; pub mod oracle_map; @@ -25,3 +28,4 @@ pub mod state; pub mod traits; pub mod user; pub mod user_map; +pub mod zero_copy; diff --git a/programs/drift/src/state/oracle.rs b/programs/drift/src/state/oracle.rs index 3becf945f6..2622e5d928 100644 --- a/programs/drift/src/state/oracle.rs +++ b/programs/drift/src/state/oracle.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +use bytemuck::{Pod, Zeroable}; use std::cell::Ref; +use std::convert::TryFrom; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; @@ -172,6 +174,55 @@ impl OracleSource { } } +impl TryFrom for OracleSource { + type Error = ErrorCode; + + fn try_from(v: u8) -> DriftResult { + match v { + 0 => Ok(OracleSource::Pyth), + 1 => Ok(OracleSource::Switchboard), + 2 => Ok(OracleSource::QuoteAsset), + 3 => Ok(OracleSource::Pyth1K), + 4 => Ok(OracleSource::Pyth1M), + 5 => Ok(OracleSource::PythStableCoin), + 6 => Ok(OracleSource::Prelaunch), + 7 => Ok(OracleSource::PythPull), + 8 => Ok(OracleSource::Pyth1KPull), + 9 => Ok(OracleSource::Pyth1MPull), + 10 => Ok(OracleSource::PythStableCoinPull), + 11 => Ok(OracleSource::SwitchboardOnDemand), + 12 => Ok(OracleSource::PythLazer), + 13 => Ok(OracleSource::PythLazer1K), + 14 => Ok(OracleSource::PythLazer1M), + 15 => Ok(OracleSource::PythLazerStableCoin), + _ => Err(ErrorCode::InvalidOracle), + } + } +} + +impl From for u8 { + fn from(src: OracleSource) -> u8 { + match src { + OracleSource::Pyth => 0, + OracleSource::Switchboard => 1, + OracleSource::QuoteAsset => 2, + OracleSource::Pyth1K => 3, + OracleSource::Pyth1M => 4, + OracleSource::PythStableCoin => 5, + OracleSource::Prelaunch => 6, + OracleSource::PythPull => 7, + OracleSource::Pyth1KPull => 8, + OracleSource::Pyth1MPull => 9, + OracleSource::PythStableCoinPull => 10, + OracleSource::SwitchboardOnDemand => 11, + OracleSource::PythLazer => 12, + OracleSource::PythLazer1K => 13, + OracleSource::PythLazer1M => 14, + OracleSource::PythLazerStableCoin => 15, + } + } +} + const MM_EXCHANGE_FALLBACK_THRESHOLD: u128 = PERCENTAGE_PRECISION / 100; // 1% #[derive(Default, Clone, Copy, Debug)] pub struct MMOraclePriceData { diff --git a/programs/drift/src/state/paused_operations.rs b/programs/drift/src/state/paused_operations.rs index 81a6ec2a3a..516470460a 100644 --- a/programs/drift/src/state/paused_operations.rs +++ b/programs/drift/src/state/paused_operations.rs @@ -97,3 +97,55 @@ impl InsuranceFundOperation { } } } + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum PerpLpOperation { + TrackAmmRevenue = 0b00000001, + SettleQuoteOwed = 0b00000010, +} + +const ALL_PERP_LP_OPERATIONS: [PerpLpOperation; 2] = [ + PerpLpOperation::TrackAmmRevenue, + PerpLpOperation::SettleQuoteOwed, +]; + +impl PerpLpOperation { + pub fn is_operation_paused(current: u8, operation: PerpLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_PERP_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + +const ALL_CONSTITUENT_LP_OPERATIONS: [ConstituentLpOperation; 3] = [ + ConstituentLpOperation::Swap, + ConstituentLpOperation::Deposit, + ConstituentLpOperation::Withdraw, +]; + +impl ConstituentLpOperation { + pub fn is_operation_paused(current: u8, operation: ConstituentLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_CONSTITUENT_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 887ed6c133..9871f39deb 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -5,31 +5,27 @@ use anchor_lang::prelude::*; use crate::state::state::{State, ValidityGuardRails}; use std::cmp::max; -use crate::controller::position::{PositionDelta, PositionDirection}; +use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; -use crate::math::amm; +use crate::math::amm::{self}; use crate::math::casting::Cast; #[cfg(test)] use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; use crate::math::constants::{ - AMM_RESERVE_PRECISION_I128, AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, - BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, - DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, FUNDING_RATE_BUFFER_I128, - FUNDING_RATE_OFFSET_PERCENTAGE, LIQUIDATION_FEE_PRECISION, - LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, LP_FEE_SLICE_DENOMINATOR, LP_FEE_SLICE_NUMERATOR, - MARGIN_PRECISION, MARGIN_PRECISION_U128, MAX_LIQUIDATION_MULTIPLIER, PEG_PRECISION, - PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, PRICE_PRECISION_I64, + AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, + BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, + FUNDING_RATE_BUFFER_I128, FUNDING_RATE_OFFSET_PERCENTAGE, LIQUIDATION_FEE_PRECISION, + LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MARGIN_PRECISION, MARGIN_PRECISION_U128, + MAX_LIQUIDATION_MULTIPLIER, PEG_PRECISION, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; -use crate::math::helpers::get_proportion_i128; use crate::math::margin::{ calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, MarginRequirementType, }; use crate::math::safe_math::SafeMath; use crate::math::stats; -use crate::state::events::OrderActionExplanation; use num_integer::Roots; use crate::state::oracle::{ @@ -91,6 +87,23 @@ impl MarketStatus { } } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum LpStatus { + /// Not considered + #[default] + Uncollateralized, + /// all operations allowed + Active, + /// Decommissioning + Decommissioning, +} + +impl LpStatus { + pub fn is_collateralized(&self) -> bool { + !matches!(self, LpStatus::Uncollateralized) + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] pub enum ContractType { #[default] @@ -236,7 +249,10 @@ pub struct PerpMarket { pub high_leverage_margin_ratio_maintenance: u16, pub protected_maker_limit_price_divisor: u8, pub protected_maker_dynamic_divisor: u8, - pub padding1: u32, + pub lp_fee_transfer_scalar: u8, + pub lp_status: u8, + pub lp_paused_operations: u8, + pub lp_exchange_fee_excluscion_scalar: u8, pub last_fill_price: u64, pub padding: [u8; 24], } @@ -280,7 +296,10 @@ impl Default for PerpMarket { high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding1: 0, + lp_fee_transfer_scalar: 0, + lp_status: 0, + lp_exchange_fee_excluscion_scalar: 0, + lp_paused_operations: 0, last_fill_price: 0, padding: [0; 24], } @@ -726,7 +745,7 @@ impl PerpMarket { let last_fill_price = self.last_fill_price; - let mark_price_5min_twap = self.amm.last_mark_price_twap_5min; + let mark_price_5min_twap = self.amm.last_mark_price_twap; let last_oracle_price_twap_5min = self.amm.historical_oracle_data.last_oracle_price_twap_5min; @@ -741,10 +760,6 @@ impl PerpMarket { let oracle_plus_funding_basis = oracle_price.safe_add(last_funding_basis)?.cast::()?; let median_price = if last_fill_price > 0 { - println!( - "last_fill_price: {} oracle_plus_funding_basis: {} oracle_plus_basis_5min: {}", - last_fill_price, oracle_plus_funding_basis, oracle_plus_basis_5min - ); let mut prices = [ last_fill_price, oracle_plus_funding_basis, @@ -1628,6 +1643,8 @@ impl AMM { #[cfg(test)] impl AMM { pub fn default_test() -> Self { + use crate::math::constants::PRICE_PRECISION_I64; + let default_reserves = 100 * AMM_RESERVE_PRECISION; // make sure tests dont have the default sqrt_k = 0 AMM { @@ -1653,6 +1670,8 @@ impl AMM { } pub fn default_btc_test() -> Self { + use crate::math::constants::PRICE_PRECISION_I64; + AMM { base_asset_reserve: 65 * AMM_RESERVE_PRECISION, quote_asset_reserve: 63015384615, diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index 46de2248b1..89d1974d08 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -234,7 +234,7 @@ mod get_trigger_price { .get_trigger_price(oracle_price, now, true) .unwrap(); - assert_eq!(trigger_price, 109144736794); + assert_eq!(trigger_price, 109147085925); } #[test] diff --git a/programs/drift/src/state/state.rs b/programs/drift/src/state/state.rs index 9e8b0961df..aeb68953b8 100644 --- a/programs/drift/src/state/state.rs +++ b/programs/drift/src/state/state.rs @@ -42,7 +42,8 @@ pub struct State { pub max_number_of_sub_accounts: u16, pub max_initialize_user_fee: u16, pub feature_bit_flags: u8, - pub padding: [u8; 9], + pub lp_pool_feature_bit_flags: u8, + pub padding: [u8; 8], } #[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] @@ -120,6 +121,18 @@ impl State { pub fn use_median_trigger_price(&self) -> bool { (self.feature_bit_flags & (FeatureBitFlags::MedianTriggerPrice as u8)) > 0 } + + pub fn allow_settle_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::SettleLpPool as u8)) > 0 + } + + pub fn allow_swap_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::SwapLpPool as u8)) > 0 + } + + pub fn allow_mint_redeem_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::MintRedeemLpPool as u8)) > 0 + } } #[derive(Clone, Copy, PartialEq, Debug, Eq)] @@ -128,6 +141,13 @@ pub enum FeatureBitFlags { MedianTriggerPrice = 0b00000010, } +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum LpPoolFeatureBitFlags { + SettleLpPool = 0b00000001, + SwapLpPool = 0b00000010, + MintRedeemLpPool = 0b00000100, +} + impl Size for State { const SIZE: usize = 992; } diff --git a/programs/drift/src/state/zero_copy.rs b/programs/drift/src/state/zero_copy.rs new file mode 100644 index 0000000000..b6a11a383f --- /dev/null +++ b/programs/drift/src/state/zero_copy.rs @@ -0,0 +1,181 @@ +use crate::error::ErrorCode; +use crate::math::safe_unwrap::SafeUnwrap; +use anchor_lang::prelude::{AccountInfo, Pubkey}; +use bytemuck::{from_bytes, from_bytes_mut}; +use bytemuck::{Pod, Zeroable}; +use std::cell::{Ref, RefMut}; +use std::marker::PhantomData; + +use crate::error::DriftResult; +use crate::msg; +use crate::validate; + +pub trait HasLen { + fn len(&self) -> u32; +} + +pub struct AccountZeroCopy<'a, T, F> { + pub fixed: Ref<'a, F>, + pub data: Ref<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopy<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub struct AccountZeroCopyMut<'a, T, F> { + pub fixed: RefMut<'a, F>, + pub data: RefMut<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopyMut<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get_mut(&mut self, index: u32) -> &mut T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes_mut(&mut self.data[start..start + size]) + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub trait ZeroCopyLoader<'a, T, F> { + fn load_zc(&'a self) -> DriftResult>; + fn load_zc_mut(&'a self) -> DriftResult>; +} + +pub fn load_generic<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner {}, program_id: {}", + acct.owner, + program_id, + )?; + + let data = acct.try_borrow_data().safe_unwrap()?; + let (disc, rest) = Ref::map_split(data, |d| d.split_at(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = Ref::map_split(rest, |d| d.split_at(hdr_size)); + let fixed = Ref::map(hdr_bytes, |b| from_bytes::(b)); + Ok(AccountZeroCopy { + fixed, + data: body, + _marker: PhantomData, + }) +} + +pub fn load_generic_mut<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner", + )?; + + let data = acct.try_borrow_mut_data().safe_unwrap()?; + let (disc, rest) = RefMut::map_split(data, |d| d.split_at_mut(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = RefMut::map_split(rest, |d| d.split_at_mut(hdr_size)); + let fixed = RefMut::map(hdr_bytes, |b| from_bytes_mut::(b)); + Ok(AccountZeroCopyMut { + fixed, + data: body, + _marker: PhantomData, + }) +} + +#[macro_export] +macro_rules! impl_zero_copy_loader { + ($Acc:ty, $ID:path, $Fixed:ty, $Elem:ty) => { + impl<'info> crate::state::zero_copy::ZeroCopyLoader<'_, $Elem, $Fixed> + for AccountInfo<'info> + { + fn load_zc<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopy<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + + fn load_zc_mut<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopyMut<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic_mut::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + } + }; +} diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 3349c7d5d7..da3893e66d 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -14,6 +14,9 @@ use solana_program::program_memory::sol_memcmp; use solana_program::sysvar; use std::convert::TryInto; +#[cfg(test)] +mod tests; + const ED25519_PROGRAM_INPUT_HEADER_LEN: usize = 2; const SIGNATURE_LEN: u16 = 64; @@ -45,6 +48,7 @@ pub struct Ed25519SignatureOffsets { pub message_instruction_index: u16, } +#[derive(Debug)] pub struct VerifiedMessage { pub signed_msg_order_params: OrderParams, pub sub_account_id: Option, @@ -60,6 +64,67 @@ fn slice_eq(a: &[u8], b: &[u8]) -> bool { a.len() == b.len() && sol_memcmp(a, b, a.len()) == 0 } +pub fn deserialize_into_verified_message( + payload: Vec, + signature: &[u8; 64], + is_delegate_signer: bool, +) -> Result { + if is_delegate_signer { + if payload.len() < 8 { + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } + let min_len: usize = std::mem::size_of::(); + let mut owned = payload; + if owned.len() < min_len { + owned.resize(min_len, 0); + } + let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize( + &mut &owned[8..], // 8 byte manual discriminator + ) + .map_err(|_| { + msg!("Invalid message encoding for is_delegate_signer = true"); + SignatureVerificationError::InvalidMessageDataSize + })?; + + return Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: None, + delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + }); + } else { + if payload.len() < 8 { + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } + let min_len: usize = std::mem::size_of::(); + let mut owned = payload; + if owned.len() < min_len { + owned.resize(min_len, 0); + } + let deserialized = SignedMsgOrderParamsMessage::deserialize( + &mut &owned[8..], // 8 byte manual discriminator + ) + .map_err(|_| { + msg!("Invalid delegate message encoding for with is_delegate_signer = false"); + SignatureVerificationError::InvalidMessageDataSize + })?; + return Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: Some(deserialized.sub_account_id), + delegate_signed_taker_pubkey: None, + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + }); + } +} + /// Check Ed25519Program instruction data verifies the given msg /// /// `ix` an Ed25519Program instruction [see](https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs)) @@ -232,45 +297,7 @@ pub fn verify_and_decode_ed25519_msg( let payload = hex::decode(payload).map_err(|_| SignatureVerificationError::InvalidMessageHex)?; - if is_delegate_signer { - let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize( - &mut &payload[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid message encoding for is_delegate_signer = true"); - SignatureVerificationError::InvalidMessageDataSize - })?; - - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: None, - delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - signature: *signature, - }); - } else { - let deserialized = SignedMsgOrderParamsMessage::deserialize( - &mut &payload[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid delegate message encoding for with is_delegate_signer = false"); - SignatureVerificationError::InvalidMessageDataSize - })?; - - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: Some(deserialized.sub_account_id), - delegate_signed_taker_pubkey: None, - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - signature: *signature, - }); - } + deserialize_into_verified_message(payload, signature, is_delegate_signer) } #[error_code] diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs new file mode 100644 index 0000000000..e016483684 --- /dev/null +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -0,0 +1,184 @@ +mod sig_verification { + use std::str::FromStr; + + use anchor_lang::prelude::Pubkey; + + use crate::controller::position::PositionDirection; + use crate::validation::sig_verification::deserialize_into_verified_message; + + #[test] + fn test_deserialize_into_verified_message_non_delegate() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 1, 0, 202, 154, 59, 0, 0, 0, 0, 0, 248, + 89, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 192, 181, 74, 13, 0, 0, 0, 0, + 1, 0, 248, 89, 13, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0, 0, 0, 0, 0, 72, 112, 54, 84, 106, + 83, 48, 107, 0, 0, + ]; + + // Test deserialization with non-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(0)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 1000); + assert_eq!(verified_message.uuid, [72, 112, 54, 84, 106, 83, 48, 107]); + assert!(verified_message.take_profit_order_params.is_none()); + assert!(verified_message.stop_loss_order_params.is_none()); + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 1); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + assert_eq!(order_params.price, 224000000u64); + 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(223000000i64)); + assert_eq!(order_params.auction_end_price, Some(224000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_non_delegate_with_tpsl() { + 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, + ]; + + // 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.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]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 242, 208, 117, 159, 92, 135, 34, 224, 147, 14, 64, 92, 7, + 25, 145, 237, 79, 35, 72, 24, 140, 13, 25, 189, 134, 243, 232, 5, 89, 37, 166, 242, 41, + 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 0, 0, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + 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, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HLr2UfL422cakKkaBG4z1bMZrcyhmzX2pHdegjM6fYXB").unwrap()) + ); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.take_profit_order_params.is_none()); + assert!(verified_message.stop_loss_order_params.is_none()); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + 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(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_tpsl() { + let signature = [1u8; 64]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, + 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, + 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, + 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, + 59, 0, 0, 0, 0, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + 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, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) + ); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 1000000000u64); + assert_eq!(tp.trigger_price, 230000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 1000000000u64); + assert_eq!(sl.trigger_price, 250000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + 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(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } +} diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index 98ab7133fb..9a00d1270e 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -6,6 +6,7 @@ import { UserAccount, UserStatsAccount, InsuranceFundStake, + ConstituentAccount, HighLeverageModeConfig, } from '../types'; import StrictEventEmitter from 'strict-event-emitter-types'; @@ -259,3 +260,22 @@ export interface HighLeverageModeConfigAccountEvents { update: void; error: (e: Error) => void; } + +export interface ConstituentAccountSubscriber { + eventEmitter: StrictEventEmitter; + isSubscribed: boolean; + + subscribe(constituentAccount?: ConstituentAccount): Promise; + sync(): Promise; + unsubscribe(): Promise; +} + +export interface ConstituentAccountEvents { + onAccountUpdate: ( + account: ConstituentAccount, + pubkey: PublicKey, + slot: number + ) => void; + update: void; + error: (e: Error) => void; +} diff --git a/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts new file mode 100644 index 0000000000..1176b86589 --- /dev/null +++ b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts @@ -0,0 +1,596 @@ +import { BufferAndSlot, ProgramAccountSubscriber, ResubOpts } from './types'; +import { AnchorProvider, Program } from '@coral-xyz/anchor'; +import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import { + AccountInfoBase, + AccountInfoWithBase58EncodedData, + AccountInfoWithBase64EncodedData, + createSolanaClient, + isAddress, + type Address, + type Commitment as GillCommitment, +} from 'gill'; +import bs58 from 'bs58'; + +export class WebSocketProgramAccountSubscriberV2 + implements ProgramAccountSubscriber +{ + subscriptionName: string; + accountDiscriminator: string; + bufferAndSlot?: BufferAndSlot; + bufferAndSlotMap: Map = new Map(); + program: Program; + decodeBuffer: (accountName: string, ix: Buffer) => T; + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void; + listenerId?: number; + resubOpts?: ResubOpts; + isUnsubscribing = false; + timeoutId?: ReturnType; + options: { filters: MemcmpFilter[]; commitment?: Commitment }; + + receivingData = false; + + // Gill client components + private rpc: ReturnType['rpc']; + private rpcSubscriptions: ReturnType< + typeof createSolanaClient + >['rpcSubscriptions']; + private abortController?: AbortController; + + // Polling logic for specific accounts + private accountsToMonitor: Set = new Set(); + private pollingIntervalMs: number = 30000; // 30 seconds + private pollingTimeouts: Map> = + new Map(); + private lastWsNotificationTime: Map = new Map(); // Track last WS notification time per account + private accountsCurrentlyPolling: Set = new Set(); // Track which accounts are being polled + private batchPollingTimeout?: ReturnType; // Single timeout for batch polling + + public constructor( + subscriptionName: string, + accountDiscriminator: string, + program: Program, + decodeBufferFn: (accountName: string, ix: Buffer) => T, + options: { filters: MemcmpFilter[]; commitment?: Commitment } = { + filters: [], + }, + resubOpts?: ResubOpts, + accountsToMonitor?: PublicKey[] // Optional list of accounts to poll + ) { + this.subscriptionName = subscriptionName; + this.accountDiscriminator = accountDiscriminator; + this.program = program; + this.decodeBuffer = decodeBufferFn; + this.resubOpts = resubOpts; + if (this.resubOpts?.resubTimeoutMs < 1000) { + console.log( + 'resubTimeoutMs should be at least 1000ms to avoid spamming resub' + ); + } + this.options = options; + this.receivingData = false; + + // Initialize accounts to monitor + if (accountsToMonitor) { + accountsToMonitor.forEach((account) => { + this.accountsToMonitor.add(account.toBase58()); + }); + } + + // Initialize gill client using the same RPC URL as the program provider + const rpcUrl = (this.program.provider as AnchorProvider).connection + .rpcEndpoint; + const { rpc, rpcSubscriptions } = createSolanaClient({ + urlOrMoniker: rpcUrl, + }); + this.rpc = rpc; + this.rpcSubscriptions = rpcSubscriptions; + } + + async subscribe( + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void + ): Promise { + if (this.listenerId != null || this.isUnsubscribing) { + return; + } + + this.onChange = onChange; + + // Create abort controller for proper cleanup + const abortController = new AbortController(); + this.abortController = abortController; + + // Subscribe to program account changes using gill's rpcSubscriptions + const programId = this.program.programId.toBase58(); + if (isAddress(programId)) { + const subscription = await this.rpcSubscriptions + .programNotifications(programId, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + filters: this.options.filters.map((filter) => ({ + memcmp: { + offset: BigInt(filter.memcmp.offset), + bytes: filter.memcmp.bytes as any, + encoding: 'base64' as const, + }, + })), + }) + .subscribe({ + abortSignal: abortController.signal, + }); + + for await (const notification of subscription) { + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.handleRpcResponse( + notification.context, + notification.value.account + ); + this.setTimeout(); + } else { + this.handleRpcResponse( + notification.context, + notification.value.account + ); + } + } + } + + this.listenerId = Math.random(); // Unique ID for logging purposes + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + this.setTimeout(); + } + + // Start monitoring for accounts that may need polling if no WS event is received + this.startMonitoringForAccounts(); + } + + protected setTimeout(): void { + if (!this.onChange) { + throw new Error('onChange callback function must be set'); + } + this.timeoutId = setTimeout( + async () => { + if (this.isUnsubscribing) { + // If we are in the process of unsubscribing, do not attempt to resubscribe + return; + } + + if (this.receivingData) { + if (this.resubOpts?.logResubMessages) { + console.log( + `No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, resubscribing` + ); + } + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + } + }, + this.resubOpts?.resubTimeoutMs + ); + } + + handleRpcResponse( + context: { slot: bigint }, + accountInfo?: AccountInfoBase & + (AccountInfoWithBase58EncodedData | AccountInfoWithBase64EncodedData) + ): void { + const newSlot = Number(context.slot); + let newBuffer: Buffer | undefined = undefined; + + if (accountInfo) { + // Extract data from gill response + if (accountInfo.data) { + // Handle different data formats from gill + if (Array.isArray(accountInfo.data)) { + // If it's a tuple [data, encoding] + const [data, encoding] = accountInfo.data; + + if (encoding === ('base58' as any)) { + // Convert base58 to buffer using bs58 + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + } + + // Convert gill's account key to PublicKey + // Note: accountInfo doesn't have a key property, we need to get it from the notification + // For now, we'll use a placeholder - this needs to be fixed based on the actual gill API + const accountId = new PublicKey('11111111111111111111111111111111'); // Placeholder + const accountIdString = accountId.toBase58(); + + const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); + + // Track WebSocket notification time for this account + this.lastWsNotificationTime.set(accountIdString, Date.now()); + + // If this account was being polled, stop polling it + if (this.accountsCurrentlyPolling.has(accountIdString)) { + this.accountsCurrentlyPolling.delete(accountIdString); + + // If no more accounts are being polled, stop batch polling + if ( + this.accountsCurrentlyPolling.size === 0 && + this.batchPollingTimeout + ) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + } + + if (!existingBufferAndSlot) { + if (newBuffer) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: newSlot, + }); + const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); + this.onChange(accountId, account, { slot: newSlot }, newBuffer); + } + return; + } + + if (newSlot < existingBufferAndSlot.slot) { + return; + } + + const oldBuffer = existingBufferAndSlot.buffer; + if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: newSlot, + }); + const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); + this.onChange(accountId, account, { slot: newSlot }, newBuffer); + } + } + + private startMonitoringForAccounts(): void { + // Clear any existing polling timeouts + this.clearPollingTimeouts(); + + // Start monitoring for each account in the accountsToMonitor set + this.accountsToMonitor.forEach((accountIdString) => { + this.startMonitoringForAccount(accountIdString); + }); + } + + private startMonitoringForAccount(accountIdString: string): void { + // Clear existing timeout for this account + const existingTimeout = this.pollingTimeouts.get(accountIdString); + if (existingTimeout) { + clearTimeout(existingTimeout); + } + + // Set up monitoring timeout - only start polling if no WS notification in 30s + const timeoutId = setTimeout(async () => { + // Check if we've received a WS notification for this account recently + const lastNotificationTime = + this.lastWsNotificationTime.get(accountIdString); + const currentTime = Date.now(); + + if ( + !lastNotificationTime || + currentTime - lastNotificationTime >= this.pollingIntervalMs + ) { + // No recent WS notification, start polling + await this.pollAccount(accountIdString); + // Schedule next poll + this.startPollingForAccount(accountIdString); + } else { + // We received a WS notification recently, continue monitoring + this.startMonitoringForAccount(accountIdString); + } + }, this.pollingIntervalMs); + + this.pollingTimeouts.set(accountIdString, timeoutId); + } + + private startPollingForAccount(accountIdString: string): void { + // Add account to polling set + this.accountsCurrentlyPolling.add(accountIdString); + + // If this is the first account being polled, start batch polling + if (this.accountsCurrentlyPolling.size === 1) { + this.startBatchPolling(); + } + } + + private startBatchPolling(): void { + // Clear existing batch polling timeout + if (this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + } + + // Set up batch polling interval + this.batchPollingTimeout = setTimeout(async () => { + await this.pollAllAccounts(); + // Schedule next batch poll + this.startBatchPolling(); + }, this.pollingIntervalMs); + } + + private async pollAllAccounts(): Promise { + try { + // Get all accounts currently being polled + const accountsToPoll = Array.from(this.accountsCurrentlyPolling); + if (accountsToPoll.length === 0) { + return; + } + + // Fetch all accounts in a single batch request + const accountAddresses = accountsToPoll.map( + (accountId) => accountId as Address + ); + const rpcResponse = await this.rpc + .getMultipleAccounts(accountAddresses, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + + // Process each account response + for (let i = 0; i < accountsToPoll.length; i++) { + const accountIdString = accountsToPoll[i]; + const accountInfo = rpcResponse.value[i]; + + if (!accountInfo) { + continue; + } + + const existingBufferAndSlot = + this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: currentSlot, + }); + const account = this.decodeBuffer( + this.accountDiscriminator, + newBuffer + ); + const accountId = new PublicKey(accountIdString); + this.onChange(accountId, account, { slot: currentSlot }, newBuffer); + } + continue; + } + + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing` + ); + } + // We missed an update, resubscribe + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + return; + } + } + } + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error batch polling accounts:`, + error + ); + } + } + } + + private async pollAccount(accountIdString: string): Promise { + try { + // Fetch current account data using gill's rpc + const accountAddress = accountIdString as Address; + const rpcResponse = await this.rpc + .getAccountInfo(accountAddress, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + if (rpcResponse.value) { + let newBuffer: Buffer | undefined = undefined; + if (rpcResponse.value.data) { + if (Array.isArray(rpcResponse.value.data)) { + const [data, encoding] = rpcResponse.value.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: currentSlot, + }); + const account = this.decodeBuffer( + this.accountDiscriminator, + newBuffer + ); + const accountId = new PublicKey(accountIdString); + this.onChange(accountId, account, { slot: currentSlot }, newBuffer); + } + } + return; + } + + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (rpcResponse.value) { + if (rpcResponse.value.data) { + if (Array.isArray(rpcResponse.value.data)) { + const [data, encoding] = rpcResponse.value.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + } + + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Polling detected missed update for account ${accountIdString}, resubscribing` + ); + } + // We missed an update, resubscribe + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + return; + } + } + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error polling account ${accountIdString}:`, + error + ); + } + } + } + + private clearPollingTimeouts(): void { + this.pollingTimeouts.forEach((timeoutId) => { + clearTimeout(timeoutId); + }); + this.pollingTimeouts.clear(); + + // Clear batch polling timeout + if (this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + + // Clear accounts currently polling + this.accountsCurrentlyPolling.clear(); + } + + unsubscribe(onResub = false): Promise { + if (!onResub) { + this.resubOpts.resubTimeoutMs = undefined; + } + this.isUnsubscribing = true; + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + + // Clear polling timeouts + this.clearPollingTimeouts(); + + // Abort the WebSocket subscription + if (this.abortController) { + this.abortController.abort('unsubscribing'); + this.abortController = undefined; + } + + this.listenerId = undefined; + this.isUnsubscribing = false; + + return Promise.resolve(); + } + + // Method to add accounts to the polling list + addAccountToMonitor(accountId: PublicKey): void { + const accountIdString = accountId.toBase58(); + this.accountsToMonitor.add(accountIdString); + + // If already subscribed, start monitoring for this account + if (this.listenerId != null && !this.isUnsubscribing) { + this.startMonitoringForAccount(accountIdString); + } + } + + // Method to remove accounts from the polling list + removeAccountFromMonitor(accountId: PublicKey): void { + const accountIdString = accountId.toBase58(); + this.accountsToMonitor.delete(accountIdString); + + // Clear monitoring timeout for this account + const timeoutId = this.pollingTimeouts.get(accountIdString); + if (timeoutId) { + clearTimeout(timeoutId); + this.pollingTimeouts.delete(accountIdString); + } + + // Remove from currently polling set if it was being polled + this.accountsCurrentlyPolling.delete(accountIdString); + + // If no more accounts are being polled, stop batch polling + if (this.accountsCurrentlyPolling.size === 0 && this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + } + + // Method to set polling interval + setPollingInterval(intervalMs: number): void { + this.pollingIntervalMs = intervalMs; + // Restart monitoring with new interval if already subscribed + if (this.listenerId != null && !this.isUnsubscribing) { + this.startMonitoringForAccounts(); + } + } +} diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 2dd95c417a..2d7926ee4c 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -1,7 +1,11 @@ import { PublicKey } from '@solana/web3.js'; import * as anchor from '@coral-xyz/anchor'; import { BN } from '@coral-xyz/anchor'; -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + getAssociatedTokenAddress, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { SpotMarketAccount, TokenProgramFlag } from '../types'; export async function getDriftStateAccountPublicKeyAndNonce( @@ -394,3 +398,111 @@ export function getIfRebalanceConfigPublicKey( programId )[0]; } + +export function getLpPoolPublicKey( + programId: PublicKey, + nameBuffer: number[] +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('lp_pool')), + Buffer.from(nameBuffer), + ], + programId + )[0]; +} + +export function getLpPoolTokenVaultPublicKey( + programId: PublicKey, + lpPool: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('LP_POOL_TOKEN_VAULT')), + lpPool.toBuffer(), + ], + programId + )[0]; +} +export function getAmmConstituentMappingPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('AMM_MAP')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentTargetBasePublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('constituent_target_base')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getConstituentVaultPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT_VAULT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getAmmCachePublicKey(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from(anchor.utils.bytes.utf8.encode('amm_cache'))], + programId + )[0]; +} + +export function getConstituentCorrelationsPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('constituent_correlations')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export async function getLpPoolTokenTokenAccountPublicKey( + lpPoolTokenMint: PublicKey, + authority: PublicKey +): Promise { + return await getAssociatedTokenAddress(lpPoolTokenMint, authority, true); +} diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 352e051bf2..9556cf9fc8 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1,4 +1,7 @@ import { + AddressLookupTableAccount, + Keypair, + LAMPORTS_PER_SOL, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, @@ -15,6 +18,12 @@ import { AssetTier, SpotFulfillmentConfigStatus, IfRebalanceConfigParams, + AddAmmConstituentMappingDatum, + TxParams, + SwapReduceOnly, + InitializeConstituentParams, + ConstituentStatus, + LPPoolAccount, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -39,9 +48,25 @@ import { getFuelOverflowAccountPublicKey, getTokenProgramForSpotMarket, getIfRebalanceConfigPublicKey, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + getConstituentPublicKey, + getConstituentVaultPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getDriftSignerPublicKey, + getConstituentCorrelationsPublicKey, } from './addresses/pda'; import { squareRootBN } from './math/utils'; -import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + createInitializeMint2Instruction, + createMintToInstruction, + createTransferCheckedInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { DriftClient } from './driftClient'; import { PEG_PRECISION, @@ -57,6 +82,11 @@ import { PROGRAM_ID as PHOENIX_PROGRAM_ID } from '@ellipsis-labs/phoenix-sdk'; import { DRIFT_ORACLE_RECEIVER_ID } from './config'; import { getFeedIdUint8Array } from './util/pythOracleUtils'; import { FUEL_RESET_LOG_ACCOUNT } from './constants/txConstants'; +import { + JupiterClient, + QuoteResponse, + SwapMode, +} from './jupiter/jupiterClient'; const OPENBOOK_PROGRAM_ID = new PublicKey( 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb' @@ -477,7 +507,13 @@ export class AdminClient extends DriftClient { ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; - const initializeMarketIx = await this.getInitializePerpMarketIx( + const ammCachePublicKey = getAmmCachePublicKey(this.program.programId); + const ammCacheAccount = await this.connection.getAccountInfo( + ammCachePublicKey + ); + const mustInitializeAmmCache = ammCacheAccount?.data == null; + + const initializeMarketIxs = await this.getInitializePerpMarketIx( marketIndex, priceOracle, baseAssetReserve, @@ -503,9 +539,10 @@ export class AdminClient extends DriftClient { concentrationCoefScale, curveUpdateIntensity, ammJitIntensity, - name + name, + mustInitializeAmmCache ); - const tx = await this.buildTransaction(initializeMarketIx); + const tx = await this.buildTransaction(initializeMarketIxs); const { txSig } = await this.sendTransaction(tx, [], this.opts); @@ -549,15 +586,21 @@ export class AdminClient extends DriftClient { concentrationCoefScale = ONE, curveUpdateIntensity = 0, ammJitIntensity = 0, - name = DEFAULT_MARKET_NAME - ): Promise { + name = DEFAULT_MARKET_NAME, + includeInitAmmCacheIx = false + ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, marketIndex ); + const ixs: TransactionInstruction[] = []; + if (includeInitAmmCacheIx) { + ixs.push(await this.getInitializeAmmCacheIx()); + } + const nameBuffer = encodeName(name); - return await this.program.instruction.initializePerpMarket( + const initPerpIx = await this.program.instruction.initializePerpMarket( marketIndex, baseAssetReserve, quoteAssetReserve, @@ -591,11 +634,129 @@ export class AdminClient extends DriftClient { : this.wallet.publicKey, oracle: priceOracle, perpMarket: perpMarketPublicKey, + ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, } ); + ixs.push(initPerpIx); + return ixs; + } + + public async initializeAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getInitializeAmmCacheIx(); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getInitializeAmmCacheIx(): Promise { + return await this.program.instruction.initializeAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async updateInitialAmmCacheInfo( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getUpdateInitialAmmCacheInfoIx( + perpMarketIndexes + ); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateInitialAmmCacheInfoIx( + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + readableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], + }); + return await this.program.instruction.updateInitialAmmCacheInfo({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); + } + + public async overrideAmmCacheInfo( + perpMarketIndex: number, + params: { + quoteOwedFromLpPool?: BN; + lastSettleTs?: BN; + lastFeePoolTokenAmount?: BN; + lastNetPnlPoolTokenAmount?: BN; + }, + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getOverrideAmmCacheInfoIx( + perpMarketIndex, + params + ); + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getOverrideAmmCacheInfoIx( + perpMarketIndex: number, + params: { + quoteOwedFromLpPool?: BN; + lastSettleTs?: BN; + lastFeePoolTokenAmount?: BN; + lastNetPnlPoolTokenAmount?: BN; + } + ): Promise { + return await this.program.instruction.overrideAmmCacheInfo( + perpMarketIndex, + Object.assign( + {}, + { + quoteOwedFromLpPool: null, + lastSettleTs: null, + lastFeePoolTokenAmount: null, + lastNetPnlPoolTokenAmount: null, + }, + params + ), + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ); } public async initializePredictionMarket( @@ -869,6 +1030,42 @@ export class AdminClient extends DriftClient { ); } + public async updatePerpMarketLpPoolStatus( + perpMarketIndex: number, + lpStatus: number + ) { + const updatePerpMarketLpPoolStatusIx = + await this.getUpdatePerpMarketLpPoolStatusIx(perpMarketIndex, lpStatus); + + const tx = await this.buildTransaction(updatePerpMarketLpPoolStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketLpPoolStatusIx( + perpMarketIndex: number, + lpStatus: number + ): Promise { + return await this.program.instruction.updatePerpMarketLpPoolStatus( + lpStatus, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ); + } + public async moveAmmToPrice( perpMarketIndex: number, targetPrice: BN @@ -1059,6 +1256,13 @@ export class AdminClient extends DriftClient { sourceVault: PublicKey ): Promise { const spotMarket = this.getQuoteSpotMarketAccount(); + const remainingAccounts = [ + { + pubkey: spotMarket.mint, + isWritable: false, + isSigner: false, + }, + ]; return await this.program.instruction.depositIntoPerpMarketFeePool(amount, { accounts: { @@ -1076,6 +1280,7 @@ export class AdminClient extends DriftClient { spotMarketVault: spotMarket.vault, tokenProgram: TOKEN_PROGRAM_ID, }, + remainingAccounts, }); } @@ -2306,6 +2511,7 @@ export class AdminClient extends DriftClient { ), oracle: oracle, oldOracle: this.getPerpMarketAccount(perpMarketIndex).amm.oracle, + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -3116,6 +3322,7 @@ export class AdminClient extends DriftClient { this.program.programId, perpMarketIndex ), + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -4610,9 +4817,9 @@ export class AdminClient extends DriftClient { ): Promise { return await this.program.instruction.zeroMmOracleFields({ accounts: { - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, state: await this.getStatePublicKey(), perpMarket: await getPerpMarketPublicKey( this.program.programId, @@ -4649,4 +4856,1325 @@ export class AdminClient extends DriftClient { } ); } + + public async updateFeatureBitFlagsSettleLpPool( + enable: boolean + ): Promise { + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsSettleLpPoolIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsSettleLpPoolIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsSettleLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateFeatureBitFlagsSwapLpPool( + enable: boolean + ): Promise { + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsSwapLpPoolIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsSwapLpPoolIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsSwapLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateFeatureBitFlagsMintRedeemLpPool( + enable: boolean + ): Promise { + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsMintRedeemLpPoolIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsMintRedeemLpPoolIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsMintRedeemLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async initializeLpPool( + name: string, + minMintFee: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair, + whitelistMint?: PublicKey + ): Promise { + const ixs = await this.getInitializeLpPoolIx( + name, + minMintFee, + maxAum, + maxSettleQuoteAmountPerMarket, + mint, + whitelistMint + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, [mint]); + return txSig; + } + + public async getInitializeLpPoolIx( + name: string, + minMintFee: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair, + whitelistMint?: PublicKey + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, encodeName(name)); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const lamports = + await this.program.provider.connection.getMinimumBalanceForRentExemption( + MINT_SIZE + ); + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: this.wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: MINT_SIZE, + lamports: Math.min(0.05 * LAMPORTS_PER_SOL, lamports), // should be 0.0014616 ? but bankrun returns 10 SOL + programId: TOKEN_PROGRAM_ID, + }); + const createMintIx = createInitializeMint2Instruction( + mint.publicKey, + 6, + lpPool, + null, + TOKEN_PROGRAM_ID + ); + + return [ + createMintAccountIx, + createMintIx, + this.program.instruction.initializeLpPool( + encodeName(name), + minMintFee, + maxAum, + maxSettleQuoteAmountPerMarket, + whitelistMint ?? PublicKey.default, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + ammConstituentMapping, + constituentTargetBase, + mint: mint.publicKey, + state: await this.getStatePublicKey(), + tokenProgram: TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }, + signers: [mint], + } + ), + ]; + } + + public async increaseLpPoolMaxAum( + name: string, + newMaxAum: BN + ): Promise { + const ixs = await this.getIncreaseLpPoolMaxAumIx(name, newMaxAum); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getIncreaseLpPoolMaxAumIx( + name: string, + newMaxAum: BN + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, encodeName(name)); + return this.program.instruction.increaseLpPoolMaxAum(newMaxAum, { + accounts: { + admin: this.wallet.publicKey, + lpPool, + state: await this.getStatePublicKey(), + }, + }); + } + + public async initializeConstituent( + lpPoolName: number[], + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const ixs = await this.getInitializeConstituentIx( + lpPoolName, + initializeConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getInitializeConstituentIx( + lpPoolName: number[], + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const spotMarketIndex = initializeConstituentParams.spotMarketIndex; + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + const constituent = getConstituentPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ); + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + + return [ + this.program.instruction.initializeConstituent( + spotMarketIndex, + initializeConstituentParams.decimals, + initializeConstituentParams.maxWeightDeviation, + initializeConstituentParams.swapFeeMin, + initializeConstituentParams.swapFeeMax, + initializeConstituentParams.maxBorrowTokenAmount, + initializeConstituentParams.oracleStalenessThreshold, + initializeConstituentParams.costToTrade, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.constituentDerivativeIndex + : null, + initializeConstituentParams.constituentDerivativeDepegThreshold != null + ? initializeConstituentParams.constituentDerivativeDepegThreshold + : ZERO, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.derivativeWeight + : ZERO, + initializeConstituentParams.volatility != null + ? initializeConstituentParams.volatility + : 10, + initializeConstituentParams.gammaExecution != null + ? initializeConstituentParams.gammaExecution + : 2, + initializeConstituentParams.gammaInventory != null + ? initializeConstituentParams.gammaInventory + : 2, + initializeConstituentParams.xi != null + ? initializeConstituentParams.xi + : 2, + initializeConstituentParams.constituentCorrelations, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentTargetBase, + constituent, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + spotMarketMint: spotMarketAccount.mint, + constituentVault: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + spotMarket: spotMarketAccount.pubkey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + signers: [], + } + ), + ]; + } + + public async updateConstituentStatus( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + const updateConstituentStatusIx = await this.getUpdateConstituentStatusIx( + constituent, + constituentStatus + ); + + const tx = await this.buildTransaction(updateConstituentStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentStatusIx( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + return await this.program.instruction.updateConstituentStatus( + constituentStatus, + { + accounts: { + constituent, + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateConstituentPausedOperations( + constituent: PublicKey, + pausedOperations: number + ): Promise { + const updateConstituentPausedOperationsIx = + await this.getUpdateConstituentPausedOperationsIx( + constituent, + pausedOperations + ); + + const tx = await this.buildTransaction(updateConstituentPausedOperationsIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentPausedOperationsIx( + constituent: PublicKey, + pausedOperations: number + ): Promise { + return await this.program.instruction.updateConstituentPausedOperations( + pausedOperations, + { + accounts: { + constituent, + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateConstituentParams( + lpPoolName: number[], + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + costToTradeBps?: number; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const ixs = await this.getUpdateConstituentParamsIx( + lpPoolName, + constituentPublicKey, + updateConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentParamsIx( + lpPoolName: number[], + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return [ + this.program.instruction.updateConstituentParams( + Object.assign( + { + maxWeightDeviation: null, + swapFeeMin: null, + swapFeeMax: null, + maxBorrowTokenAmount: null, + oracleStalenessThreshold: null, + costToTradeBps: null, + stablecoinWeight: null, + derivativeWeight: null, + constituentDerivativeIndex: null, + volatility: null, + gammaExecution: null, + gammaInventory: null, + xi: null, + }, + updateConstituentParams + ), + { + accounts: { + admin: this.wallet.publicKey, + constituent: constituentPublicKey, + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ), + }, + signers: [], + } + ), + ]; + } + + public async updateLpPoolParams( + lpPoolName: number[], + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + volatility?: BN; + gammaExecution?: number; + xi?: number; + whitelistMint?: PublicKey; + } + ): Promise { + const ixs = await this.getUpdateLpPoolParamsIx( + lpPoolName, + updateLpPoolParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateLpPoolParamsIx( + lpPoolName: number[], + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + volatility?: BN; + gammaExecution?: number; + xi?: number; + whitelistMint?: PublicKey; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return [ + this.program.instruction.updateLpPoolParams( + Object.assign( + { + maxSettleQuoteAmount: null, + volatility: null, + gammaExecution: null, + xi: null, + whitelistMint: null, + }, + updateLpPoolParams + ), + { + accounts: { + admin: this.wallet.publicKey, + state: await this.getStatePublicKey(), + lpPool, + }, + signers: [], + } + ), + ]; + } + + public async addAmmConstituentMappingData( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getAddAmmConstituentMappingDataIx( + lpPoolName, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getAddAmmConstituentMappingDataIx( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.addAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + constituentTargetBase, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateAmmConstituentMappingData( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getUpdateAmmConstituentMappingDataIx( + lpPoolName, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateAmmConstituentMappingDataIx( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.updateAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async removeAmmConstituentMappingData( + lpPoolName: number[], + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const ixs = await this.getRemoveAmmConstituentMappingDataIx( + lpPoolName, + perpMarketIndex, + constituentIndex + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getRemoveAmmConstituentMappingDataIx( + lpPoolName: number[], + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + + return [ + this.program.instruction.removeAmmConstituentMappingData( + perpMarketIndex, + constituentIndex, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateConstituentCorrelationData( + lpPoolName: number[], + index1: number, + index2: number, + correlation: BN + ): Promise { + const ixs = await this.getUpdateConstituentCorrelationDataIx( + lpPoolName, + index1, + index2, + correlation + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentCorrelationDataIx( + lpPoolName: number[], + index1: number, + index2: number, + correlation: BN + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return [ + this.program.instruction.updateConstituentCorrelationData( + index1, + index2, + correlation, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + /** + * Get the drift begin_swap and end_swap instructions + * + * @param outMarketIndex the market index of the token you're buying + * @param inMarketIndex the market index of the token you're selling + * @param amountIn the amount of the token to sell + * @param inTokenAccount the token account to move the tokens being sold (admin signer ata for lp swap) + * @param outTokenAccount the token account to receive the tokens being bought (admin signer ata for lp swap) + * @param limitPrice the limit price of the swap + * @param reduceOnly + * @param userAccountPublicKey optional, specify a custom userAccountPublicKey to use instead of getting the current user account; can be helpful if the account is being created within the current tx + */ + public async getSwapIx( + { + lpPoolName, + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }: { + lpPoolName: number[]; + outMarketIndex: number; + inMarketIndex: number; + amountIn: BN; + inTokenAccount: PublicKey; + outTokenAccount: PublicKey; + limitPrice?: BN; + reduceOnly?: SwapReduceOnly; + userAccountPublicKey?: PublicKey; + }, + lpSwap?: boolean + ): Promise<{ + beginSwapIx: TransactionInstruction; + endSwapIx: TransactionInstruction; + }> { + if (!lpSwap) { + return super.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }); + } + const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); + const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); + + const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); + const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); + + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const outConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const beginSwapIx = this.program.instruction.beginLpSwap( + inMarketIndex, + outMarketIndex, + amountIn, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + tokenProgram: inTokenProgram, + }, + } + ); + + const remainingAccounts = []; + remainingAccounts.push({ + pubkey: outTokenProgram, + isWritable: false, + isSigner: false, + }); + + const endSwapIx = this.program.instruction.endLpSwap( + inMarketIndex, + outMarketIndex, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + tokenProgram: inTokenProgram, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }, + remainingAccounts, + } + ); + + return { beginSwapIx, endSwapIx }; + } + + public async getLpJupiterSwapIxV6({ + jupiterClient, + outMarketIndex, + inMarketIndex, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + quote, + lpPoolName, + }: { + jupiterClient: JupiterClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: QuoteResponse; + lpPoolName: number[]; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + if (!quote) { + const fetchedQuote = await jupiterClient.getQuote({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + + quote = fetchedQuote; + } + + if (!quote) { + throw new Error("Could not fetch Jupiter's quote. Please try again."); + } + + const isExactOut = swapMode === 'ExactOut' || quote.swapMode === 'ExactOut'; + const amountIn = new BN(quote.inAmount); + const exactOutBufferedAmountIn = amountIn.muln(1001).divn(1000); // Add 10bp buffer + + const transaction = await jupiterClient.getSwap({ + quote, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + }); + + const { transactionMessage, lookupTables } = + await jupiterClient.getTransactionMessageAndLookupTables({ + transaction, + }); + + const jupiterInstructions = jupiterClient.getJupiterInstructions({ + transactionMessage, + inputMint: inMarket.mint, + outputMint: outMarket.mint, + }); + + const preInstructions = []; + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + const outAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const outAccountInfo = await this.connection.getAccountInfo( + outAssociatedTokenAccount + ); + if (!outAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + outAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + + const inTokenProgram = this.getTokenProgramForSpotMarket(inMarket); + const inAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + inTokenProgram + ); + + const inAccountInfo = await this.connection.getAccountInfo( + inAssociatedTokenAccount + ); + if (!inAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + inAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + lpPoolName, + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amountIn, + inTokenAccount: inAssociatedTokenAccount, + outTokenAccount: outAssociatedTokenAccount, + }); + + const ixs = [ + ...preInstructions, + beginSwapIx, + ...jupiterInstructions, + endSwapIx, + ]; + + return { ixs, lookupTables }; + } + + public async getDevnetLpSwapIxs( + amountIn: BN, + amountOut: BN, + externalUserAuthority: PublicKey, + externalUserInTokenAccount: PublicKey, + externalUserOutTokenAccount: PublicKey, + inSpotMarketIndex: number, + outSpotMarketIndex: number + ): Promise { + const inSpotMarketAccount = this.getSpotMarketAccount(inSpotMarketIndex); + const outSpotMarketAccount = this.getSpotMarketAccount(outSpotMarketIndex); + + const outTokenAccount = await this.getAssociatedTokenAccount( + outSpotMarketAccount.marketIndex, + false, + getTokenProgramForSpotMarket(outSpotMarketAccount) + ); + const inTokenAccount = await this.getAssociatedTokenAccount( + inSpotMarketAccount.marketIndex, + false, + getTokenProgramForSpotMarket(inSpotMarketAccount) + ); + + const externalCreateInTokenAccountIx = + this.createAssociatedTokenAccountIdempotentInstruction( + externalUserInTokenAccount, + this.wallet.publicKey, + externalUserAuthority, + this.getSpotMarketAccount(inSpotMarketIndex)!.mint + ); + + const externalCreateOutTokenAccountIx = + this.createAssociatedTokenAccountIdempotentInstruction( + externalUserOutTokenAccount, + this.wallet.publicKey, + externalUserAuthority, + this.getSpotMarketAccount(outSpotMarketIndex)!.mint + ); + + const outTransferIx = createTransferCheckedInstruction( + externalUserOutTokenAccount, + outSpotMarketAccount.mint, + outTokenAccount, + externalUserAuthority, + amountOut.toNumber(), + outSpotMarketAccount.decimals, + undefined, + getTokenProgramForSpotMarket(outSpotMarketAccount) + ); + + const inTransferIx = createTransferCheckedInstruction( + inTokenAccount, + inSpotMarketAccount.mint, + externalUserInTokenAccount, + this.wallet.publicKey, + amountIn.toNumber(), + inSpotMarketAccount.decimals, + undefined, + getTokenProgramForSpotMarket(inSpotMarketAccount) + ); + + const ixs = [ + externalCreateInTokenAccountIx, + externalCreateOutTokenAccountIx, + outTransferIx, + inTransferIx, + ]; + return ixs; + } + + public async getAllDevnetLpSwapIxs( + lpPoolName: number[], + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + externalUserAuthority: PublicKey + ) { + const { beginSwapIx, endSwapIx } = await this.getSwapIx( + { + lpPoolName, + inMarketIndex, + outMarketIndex, + amountIn: inAmount, + inTokenAccount: await this.getAssociatedTokenAccount( + inMarketIndex, + false + ), + outTokenAccount: await this.getAssociatedTokenAccount( + outMarketIndex, + false + ), + }, + true + ); + + const devnetLpSwapIxs = await this.getDevnetLpSwapIxs( + inAmount, + minOutAmount, + externalUserAuthority, + await this.getAssociatedTokenAccount( + inMarketIndex, + false, + getTokenProgramForSpotMarket(this.getSpotMarketAccount(inMarketIndex)), + externalUserAuthority + ), + await this.getAssociatedTokenAccount( + outMarketIndex, + false, + getTokenProgramForSpotMarket(this.getSpotMarketAccount(outMarketIndex)), + externalUserAuthority + ), + inMarketIndex, + outMarketIndex + ); + + return [ + beginSwapIx, + ...devnetLpSwapIxs, + endSwapIx, + ] as TransactionInstruction[]; + } + + public async depositWithdrawToProgramVault( + lpPoolName: number[], + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise { + const { depositIx, withdrawIx } = + await this.getDepositWithdrawToProgramVaultIxs( + lpPoolName, + depositMarketIndex, + borrowMarketIndex, + amountToDeposit, + amountToBorrow + ); + + const tx = await this.buildTransaction([depositIx, withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositWithdrawToProgramVaultIxs( + lpPoolName: number[], + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise<{ + depositIx: TransactionInstruction; + withdrawIx: TransactionInstruction; + }> { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const depositSpotMarket = this.getSpotMarketAccount(depositMarketIndex); + const withdrawSpotMarket = this.getSpotMarketAccount(borrowMarketIndex); + + const depositTokenProgram = + this.getTokenProgramForSpotMarket(depositSpotMarket); + const withdrawTokenProgram = + this.getTokenProgramForSpotMarket(withdrawSpotMarket); + + const depositConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositIx = this.program.instruction.depositToProgramVault( + amountToDeposit, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: depositConstituent, + constituentTokenAccount: depositConstituentTokenAccount, + spotMarket: depositSpotMarket.pubkey, + spotMarketVault: depositSpotMarket.vault, + tokenProgram: depositTokenProgram, + mint: depositSpotMarket.mint, + oracle: depositSpotMarket.oracle, + }, + } + ); + + const withdrawIx = this.program.instruction.withdrawFromProgramVault( + amountToBorrow, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: withdrawConstituent, + constituentTokenAccount: withdrawConstituentTokenAccount, + spotMarket: withdrawSpotMarket.pubkey, + spotMarketVault: withdrawSpotMarket.vault, + tokenProgram: withdrawTokenProgram, + mint: withdrawSpotMarket.mint, + driftSigner: getDriftSignerPublicKey(this.program.programId), + oracle: withdrawSpotMarket.oracle, + }, + } + ); + + return { depositIx, withdrawIx }; + } + + public async depositToProgramVault( + lpPoolName: number[], + depositMarketIndex: number, + amountToDeposit: BN + ): Promise { + const depositIx = await this.getDepositToProgramVaultIx( + lpPoolName, + depositMarketIndex, + amountToDeposit + ); + + const tx = await this.buildTransaction([depositIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async withdrawFromProgramVault( + lpPoolName: number[], + borrowMarketIndex: number, + amountToWithdraw: BN + ): Promise { + const withdrawIx = await this.getWithdrawFromProgramVaultIx( + lpPoolName, + borrowMarketIndex, + amountToWithdraw + ); + const tx = await this.buildTransaction([withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositToProgramVaultIx( + lpPoolName: number[], + depositMarketIndex: number, + amountToDeposit: BN + ): Promise { + const { depositIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolName, + depositMarketIndex, + depositMarketIndex, + amountToDeposit, + new BN(0) + ); + return depositIx; + } + + public async getWithdrawFromProgramVaultIx( + lpPoolName: number[], + borrowMarketIndex: number, + amountToWithdraw: BN + ): Promise { + const { withdrawIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolName, + borrowMarketIndex, + borrowMarketIndex, + new BN(0), + amountToWithdraw + ); + return withdrawIx; + } + + public async updatePerpMarketLpPoolFeeTransferScalar( + marketIndex: number, + lpFeeTransferScalar?: number, + lpExchangeFeeExcluscionScalar?: number + ) { + const ix = await this.getUpdatePerpMarketLpPoolFeeTransferScalarIx( + marketIndex, + lpFeeTransferScalar, + lpExchangeFeeExcluscionScalar + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdatePerpMarketLpPoolFeeTransferScalarIx( + marketIndex: number, + lpFeeTransferScalar?: number, + lpExchangeFeeExcluscionScalar?: number + ): Promise { + return this.program.instruction.updatePerpMarketLpPoolFeeTransferScalar( + lpFeeTransferScalar ?? null, + lpExchangeFeeExcluscionScalar ?? null, + { + accounts: { + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, + }, + } + ); + } + + public async updatePerpMarketLpPoolPausedOperations( + marketIndex: number, + pausedOperations: number + ) { + const ix = await this.getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex, + pausedOperations + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex: number, + pausedOperations: number + ): Promise { + return this.program.instruction.updatePerpMarketLpPoolPausedOperations( + pausedOperations, + { + accounts: { + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, + }, + } + ); + } + + public async mintLpWhitelistToken( + lpPool: LPPoolAccount, + authority: PublicKey + ): Promise { + const ix = await this.getMintLpWhitelistTokenIx(lpPool, authority); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getMintLpWhitelistTokenIx( + lpPool: LPPoolAccount, + authority: PublicKey + ): Promise { + const mintAmount = 1000; + const associatedTokenAccount = getAssociatedTokenAddressSync( + lpPool.whitelistMint, + authority, + false + ); + + const ixs: TransactionInstruction[] = []; + const createInstruction = + this.createAssociatedTokenAccountIdempotentInstruction( + associatedTokenAccount, + this.wallet.publicKey, + authority, + lpPool.whitelistMint + ); + ixs.push(createInstruction); + const mintToInstruction = createMintToInstruction( + lpPool.whitelistMint, + associatedTokenAccount, + this.wallet.publicKey, + mintAmount, + [], + TOKEN_PROGRAM_ID + ); + ixs.push(mintToInstruction); + return ixs; + } } diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts new file mode 100644 index 0000000000..59a4d88d22 --- /dev/null +++ b/sdk/src/constituentMap/constituentMap.ts @@ -0,0 +1,291 @@ +import { + Commitment, + Connection, + MemcmpFilter, + PublicKey, + RpcResponseAndContext, +} from '@solana/web3.js'; +import { ConstituentAccountSubscriber, DataAndSlot } from '../accounts/types'; +import { ConstituentAccount } from '../types'; +import { PollingConstituentAccountSubscriber } from './pollingConstituentAccountSubscriber'; +import { WebSocketConstituentAccountSubscriber } from './webSocketConstituentAccountSubscriber'; +import { DriftClient } from '../driftClient'; +import { getConstituentFilter, getConstituentLpPoolFilter } from '../memcmp'; +import { ZSTDDecoder } from 'zstddec'; +import { encodeName } from '../userName'; +import { getLpPoolPublicKey } from '../addresses/pda'; + +const MAX_CONSTITUENT_SIZE_BYTES = 304; // TODO: update this when account is finalized + +const LP_POOL_NAME = 'test lp pool 2'; + +export type ConstituentMapConfig = { + driftClient: DriftClient; + connection?: Connection; + subscriptionConfig: + | { + type: 'polling'; + frequency: number; + commitment?: Commitment; + } + | { + type: 'websocket'; + resubTimeoutMs?: number; + logResubMessages?: boolean; + commitment?: Commitment; + }; + lpPoolName?: string; + // potentially use these to filter Constituent accounts + additionalFilters?: MemcmpFilter[]; +}; + +export interface ConstituentMapInterface { + subscribe(): Promise; + unsubscribe(): Promise; + has(key: string): boolean; + get(key: string): ConstituentAccount | undefined; + getFromSpotMarketIndex( + spotMarketIndex: number + ): ConstituentAccount | undefined; + getFromConstituentIndex( + constituentIndex: number + ): ConstituentAccount | undefined; + + getWithSlot(key: string): DataAndSlot | undefined; + mustGet(key: string): Promise; + mustGetWithSlot(key: string): Promise>; +} + +export class ConstituentMap implements ConstituentMapInterface { + private driftClient: DriftClient; + private constituentMap = new Map>(); + private constituentAccountSubscriber: ConstituentAccountSubscriber; + private additionalFilters?: MemcmpFilter[]; + private commitment?: Commitment; + private connection?: Connection; + + private constituentIndexToKeyMap = new Map(); + private spotMarketIndexToKeyMap = new Map(); + + private lpPoolName: string; + + constructor(config: ConstituentMapConfig) { + this.driftClient = config.driftClient; + this.additionalFilters = config.additionalFilters; + this.commitment = config.subscriptionConfig.commitment; + this.connection = config.connection || this.driftClient.connection; + this.lpPoolName = config.lpPoolName ?? LP_POOL_NAME; + + if (config.subscriptionConfig.type === 'polling') { + this.constituentAccountSubscriber = + new PollingConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.frequency, + config.subscriptionConfig.commitment, + this.getFilters() + ); + } else if (config.subscriptionConfig.type === 'websocket') { + this.constituentAccountSubscriber = + new WebSocketConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.resubTimeoutMs, + config.subscriptionConfig.commitment, + this.getFilters() + ); + } + + // Listen for account updates from the subscriber + this.constituentAccountSubscriber.eventEmitter.on( + 'onAccountUpdate', + (account: ConstituentAccount, pubkey: PublicKey, slot: number) => { + this.updateConstituentAccount(pubkey.toString(), account, slot); + } + ); + } + + private getFilters(): MemcmpFilter[] { + const filters = [ + getConstituentFilter(), + getConstituentLpPoolFilter( + getLpPoolPublicKey( + this.driftClient.program.programId, + encodeName(this.lpPoolName) + ) + ), + ]; + if (this.additionalFilters) { + filters.push(...this.additionalFilters); + } + return filters; + } + + private decode(name: string, buffer: Buffer): ConstituentAccount { + return this.driftClient.program.account.constituent.coder.accounts.decodeUnchecked( + name, + buffer + ); + } + + public async sync(): Promise { + try { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.commitment, + filters: this.getFilters(), + encoding: 'base64+zstd', + withContext: true, + }, + ]; + + // @ts-ignore + const rpcJSONResponse: any = await this.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ pubkey: PublicKey; account: { data: [string, string] } }> + > = rpcJSONResponse.result; + const slot = rpcResponseAndContext.context.slot; + + const promises = rpcResponseAndContext.value.map( + async (programAccount) => { + const compressedUserData = Buffer.from( + programAccount.account.data[0], + 'base64' + ); + const decoder = new ZSTDDecoder(); + await decoder.init(); + const buffer = Buffer.from( + decoder.decode(compressedUserData, MAX_CONSTITUENT_SIZE_BYTES) + ); + const key = programAccount.pubkey.toString(); + const currAccountWithSlot = this.getWithSlot(key); + + if (currAccountWithSlot) { + if (slot >= currAccountWithSlot.slot) { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } else { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } + ); + await Promise.all(promises); + } catch (error) { + console.log(`ConstituentMap.sync() error: ${error.message}`); + } + } + + public async subscribe(): Promise { + await this.constituentAccountSubscriber.subscribe(); + } + + public async unsubscribe(): Promise { + await this.constituentAccountSubscriber.unsubscribe(); + this.constituentMap.clear(); + } + + public has(key: string): boolean { + return this.constituentMap.has(key); + } + + public get(key: string): ConstituentAccount | undefined { + return this.constituentMap.get(key)?.data; + } + + public getFromConstituentIndex( + constituentIndex: number + ): ConstituentAccount | undefined { + const key = this.constituentIndexToKeyMap.get(constituentIndex); + return key ? this.get(key) : undefined; + } + + public getFromSpotMarketIndex( + spotMarketIndex: number + ): ConstituentAccount | undefined { + const key = this.spotMarketIndexToKeyMap.get(spotMarketIndex); + return key ? this.get(key) : undefined; + } + + public getWithSlot(key: string): DataAndSlot | undefined { + return this.constituentMap.get(key); + } + + public async mustGet(key: string): Promise { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result.data; + } + + public async mustGetWithSlot( + key: string + ): Promise> { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result; + } + + public size(): number { + return this.constituentMap.size; + } + + public *values(): IterableIterator { + for (const dataAndSlot of this.constituentMap.values()) { + yield dataAndSlot.data; + } + } + + public valuesWithSlot(): IterableIterator> { + return this.constituentMap.values(); + } + + public *entries(): IterableIterator<[string, ConstituentAccount]> { + for (const [key, dataAndSlot] of this.constituentMap.entries()) { + yield [key, dataAndSlot.data]; + } + } + + public entriesWithSlot(): IterableIterator< + [string, DataAndSlot] + > { + return this.constituentMap.entries(); + } + + public updateConstituentAccount( + key: string, + constituentAccount: ConstituentAccount, + slot: number + ): void { + const existingData = this.getWithSlot(key); + if (existingData) { + if (slot >= existingData.slot) { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + } else { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + this.constituentIndexToKeyMap.set(constituentAccount.constituentIndex, key); + this.spotMarketIndexToKeyMap.set(constituentAccount.spotMarketIndex, key); + } +} diff --git a/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..e50b34df4b --- /dev/null +++ b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts @@ -0,0 +1,97 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, MemcmpFilter } from '@solana/web3.js'; +import { ConstituentMap } from './constituentMap'; + +export class PollingConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + program: Program; + frequency: number; + commitment?: Commitment; + additionalFilters?: MemcmpFilter[]; + eventEmitter: StrictEventEmitter; + + intervalId?: NodeJS.Timeout; + constituentMap: ConstituentMap; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + frequency: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.frequency = frequency; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + this.eventEmitter = new EventEmitter(); + } + + async subscribe(): Promise { + if (this.isSubscribed || this.frequency <= 0) { + return true; + } + + const executeSync = async () => { + await this.sync(); + this.intervalId = setTimeout(executeSync, this.frequency); + }; + + // Initial sync + await this.sync(); + + // Start polling + this.intervalId = setTimeout(executeSync, this.frequency); + + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `PollingConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + if (this.intervalId) { + clearTimeout(this.intervalId); + this.intervalId = undefined; + } + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } + + didSubscriptionSucceed(): boolean { + return this.isSubscribed; + } +} diff --git a/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..816acd6211 --- /dev/null +++ b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts @@ -0,0 +1,112 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import { ConstituentAccount } from '../types'; +import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber'; +import { getConstituentFilter } from '../memcmp'; +import { ConstituentMap } from './constituentMap'; + +export class WebSocketConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + resubTimeoutMs?: number; + commitment?: Commitment; + program: Program; + eventEmitter: StrictEventEmitter; + + constituentDataAccountSubscriber: WebSocketProgramAccountSubscriber; + constituentMap: ConstituentMap; + private additionalFilters?: MemcmpFilter[]; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + resubTimeoutMs?: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.eventEmitter = new EventEmitter(); + this.resubTimeoutMs = resubTimeoutMs; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + } + + async subscribe(): Promise { + if (this.isSubscribed) { + return true; + } + this.constituentDataAccountSubscriber = + new WebSocketProgramAccountSubscriber( + 'LpPoolConstituent', + 'Constituent', + this.program, + this.program.account.constituent.coder.accounts.decode.bind( + this.program.account.constituent.coder.accounts + ), + { + filters: [getConstituentFilter(), ...(this.additionalFilters || [])], + commitment: this.commitment, + } + ); + + await this.constituentDataAccountSubscriber.subscribe( + (accountId: PublicKey, account: ConstituentAccount, context: Context) => { + this.constituentMap.updateConstituentAccount( + accountId.toBase58(), + account, + context.slot + ); + this.eventEmitter.emit( + 'onAccountUpdate', + account, + accountId, + context.slot + ); + } + ); + + this.eventEmitter.emit('update'); + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `WebSocketConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + await Promise.all([this.constituentDataAccountSubscriber.unsubscribe()]); + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 4a02efca43..a143064366 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -65,6 +65,10 @@ import { SignedMsgOrderParamsDelegateMessage, TokenProgramFlag, PostOnlyParams, + LPPoolAccount, + ConstituentAccount, + ConstituentTargetBaseAccount, + AmmCache, } from './types'; import driftIDL from './idl/drift.json'; @@ -113,6 +117,15 @@ import { getUserStatsAccountPublicKey, getSignedMsgWsDelegatesAccountPublicKey, getIfRebalanceConfigPublicKey, + getConstituentTargetBasePublicKey, + getAmmConstituentMappingPublicKey, + getLpPoolPublicKey, + getConstituentPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getConstituentVaultPublicKey, + getConstituentCorrelationsPublicKey, + getLpPoolTokenTokenAccountPublicKey, } from './addresses/pda'; import { DataAndSlot, @@ -137,6 +150,7 @@ import { decodeName, DEFAULT_USER_NAME, encodeName } from './userName'; import { MMOraclePriceData, OraclePriceData } from './oracles/types'; import { DriftClientConfig } from './driftClientConfig'; import { PollingDriftClientAccountSubscriber } from './accounts/pollingDriftClientAccountSubscriber'; +import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; import { RetryTxSender } from './tx/retryTxSender'; import { User } from './user'; import { UserSubscriptionConfig } from './userConfig'; @@ -193,8 +207,7 @@ import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; -import { Commitment } from 'gill'; -import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; +import { ConstituentMap } from './constituentMap/constituentMap'; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -372,8 +385,6 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, commitment: config.accountSubscription?.commitment, - programUserAccountSubscriber: - config.accountSubscription?.programUserAccountSubscriber, }; this.userStatsAccountSubscriptionConfig = { type: 'websocket', @@ -439,10 +450,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 ?? [], @@ -453,7 +461,9 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, }, - config.accountSubscription?.commitment as Commitment + config.accountSubscription?.commitment, + config.accountSubscription?.perpMarketAccountSubscriber, + config.accountSubscription?.oracleAccountSubscriber ); } this.eventEmitter = this.accountSubscriber.eventEmitter; @@ -614,8 +624,7 @@ export class DriftClient { public getSpotMarketAccount( marketIndex: number ): SpotMarketAccount | undefined { - return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex) - ?.data; + return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } /** @@ -626,8 +635,7 @@ export class DriftClient { marketIndex: number ): Promise { await this.accountSubscriber.fetch(); - return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex) - ?.data; + return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } public getSpotMarketAccounts(): SpotMarketAccount[] { @@ -2536,17 +2544,19 @@ export class DriftClient { public async getAssociatedTokenAccount( marketIndex: number, useNative = true, - tokenProgram = TOKEN_PROGRAM_ID + tokenProgram = TOKEN_PROGRAM_ID, + authority = this.wallet.publicKey, + allowOwnerOffCurve = false ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); if (useNative && spotMarket.mint.equals(WRAPPED_SOL_MINT)) { - return this.wallet.publicKey; + return authority; } const mint = spotMarket.mint; return await getAssociatedTokenAddress( mint, - this.wallet.publicKey, - undefined, + authority, + allowOwnerOffCurve, tokenProgram ); } @@ -10283,6 +10293,1040 @@ export class DriftClient { }); } + public async getLpPoolAccount(lpPoolName: number[]): Promise { + return (await this.program.account.lpPool.fetch( + getLpPoolPublicKey(this.program.programId, lpPoolName) + )) as LPPoolAccount; + } + + public async getConstituentTargetBaseAccount( + lpPoolName: number[] + ): Promise { + return (await this.program.account.constituentTargetBase.fetch( + getConstituentTargetBasePublicKey( + this.program.programId, + getLpPoolPublicKey(this.program.programId, lpPoolName) + ) + )) as ConstituentTargetBaseAccount; + } + + public async getAmmCache(): Promise { + return (await this.program.account.ammCache.fetch( + getAmmCachePublicKey(this.program.programId) + )) as AmmCache; + } + + public async updateLpConstituentTargetBase( + lpPoolName: number[], + constituents: PublicKey[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpConstituentTargetBaseIx(lpPoolName, constituents), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateLpConstituentTargetBaseIx( + lpPoolName: number[], + constituents: PublicKey[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMappingPublicKey = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const ammCache = getAmmCachePublicKey(this.program.programId); + + const remainingAccounts = constituents.map((constituent) => { + return { + isWritable: false, + isSigner: false, + pubkey: constituent, + }; + }); + + return this.program.instruction.updateLpConstituentTargetBase({ + accounts: { + keeper: this.wallet.publicKey, + lpPool, + ammConstituentMapping: ammConstituentMappingPublicKey, + constituentTargetBase, + state: await this.getStatePublicKey(), + ammCache, + }, + remainingAccounts, + }); + } + + public async updateLpPoolAum( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpPoolAumIxs(lpPool, spotMarketIndexOfConstituents), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateLpPoolAumIxs( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: spotMarketIndexOfConstituents, + }); + remainingAccounts.push( + ...spotMarketIndexOfConstituents.map((index) => { + return { + pubkey: getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + index + ), + isSigner: false, + isWritable: true, + }; + }) + ); + return this.program.instruction.updateLpPoolAum({ + accounts: { + keeper: this.wallet.publicKey, + lpPool: lpPool.pubkey, + state: await this.getStatePublicKey(), + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ), + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); + } + + public async updateAmmCache( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateAmmCacheIx(perpMarketIndexes), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateAmmCacheIx( + perpMarketIndexes: number[] + ): Promise { + if (perpMarketIndexes.length > 50) { + throw new Error('Cant update more than 50 markets at once'); + } + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + }); + + return this.program.instruction.updateAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: this.getSpotMarketAccount(0).pubkey, + }, + remainingAccounts, + }); + } + + public async updateConstituentOracleInfo( + constituent: ConstituentAccount + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateConstituentOracleInfoIx(constituent), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateConstituentOracleInfoIx( + constituent: ConstituentAccount + ): Promise { + const spotMarket = this.getSpotMarketAccount(constituent.spotMarketIndex); + return this.program.instruction.updateConstituentOracleInfo({ + accounts: { + keeper: this.wallet.publicKey, + constituent: constituent.pubkey, + state: await this.getStatePublicKey(), + oracle: spotMarket.oracle, + spotMarket: spotMarket.pubkey, + }, + }); + } + + public async lpPoolSwap( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + userInTokenAccount: PublicKey, + userOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey, + inMarketMint: PublicKey, + outMarketMint: PublicKey, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolSwapIx( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + userInTokenAccount, + userOutTokenAccount, + inConstituent, + outConstituent, + inMarketMint, + outMarketMint + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolSwapIx( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + userInTokenAccount: PublicKey, + userOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey, + inMarketMint: PublicKey, + outMarketMint: PublicKey + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], + }); + + return this.program.instruction.lpPoolSwap( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + userInTokenAccount, + userOutTokenAccount, + inConstituent, + outConstituent, + inMarketMint, + outMarketMint, + authority: this.wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async viewLpPoolSwapFees( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + inTargetWeight: BN, + outTargetWeight: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolSwapFeesIx( + inMarketIndex, + outMarketIndex, + inAmount, + inTargetWeight, + outTargetWeight, + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + inConstituent, + outConstituent + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolSwapFeesIx( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + inTargetWeight: BN, + outTargetWeight: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], + }); + + return this.program.instruction.viewLpPoolSwapFees( + inMarketIndex, + outMarketIndex, + inAmount, + inTargetWeight, + outTargetWeight, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + inConstituent, + outConstituent, + authority: this.wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async getCreateLpPoolTokenAccountIx( + lpPool: LPPoolAccount + ): Promise { + const lpMint = lpPool.mint; + const userLpTokenAccount = await getLpPoolTokenTokenAccountPublicKey( + lpMint, + this.wallet.publicKey + ); + + return this.createAssociatedTokenAccountIdempotentInstruction( + userLpTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey, + lpMint + ); + } + + public async createLpPoolTokenAccount( + lpPool: LPPoolAccount, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getCreateLpPoolTokenAccountIx(lpPool), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async lpPoolAddLiquidity({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + txParams, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + }): Promise { + const ixs: TransactionInstruction[] = []; + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [inMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(inMarketIndex); + const inMarketMint = spotMarket.mint; + const isSolMarket = inMarketMint.equals(WRAPPED_SOL_MINT); + + let wSolTokenAccount: PublicKey | undefined; + if (isSolMarket) { + const { ixs: wSolIxs, pubkey } = + await this.getWrappedSolAccountCreationIxs(inAmount, true); + wSolTokenAccount = pubkey; + ixs.push(...wSolIxs); + } + + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const userInTokenAccount = + wSolTokenAccount ?? + (await this.getAssociatedTokenAccount(inMarketIndex, false)); + const constituentInTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getLpPoolTokenTokenAccountPublicKey( + lpMint, + this.wallet.publicKey + ); + if (!(await this.checkIfAccountExists(userLpTokenAccount))) { + ixs.push( + this.createAssociatedTokenAccountIdempotentInstruction( + userLpTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey, + lpMint + ) + ); + } + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + if (!lpPool.whitelistMint.equals(PublicKey.default)) { + const associatedTokenPublicKey = await getAssociatedTokenAddress( + lpPool.whitelistMint, + this.wallet.publicKey + ); + remainingAccounts.push({ + pubkey: associatedTokenPublicKey, + isWritable: false, + isSigner: false, + }); + } + + const lpPoolAddLiquidityIx = this.program.instruction.lpPoolAddLiquidity( + inMarketIndex, + inAmount, + minMintAmount, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + inMarketMint, + inConstituent, + userInTokenAccount, + constituentInTokenAccount, + userLpTokenAccount, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + ixs.push(lpPoolAddLiquidityIx); + + if (isSolMarket && wSolTokenAccount) { + ixs.push( + createCloseAccountInstruction( + wSolTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey + ) + ); + } + return [...ixs]; + } + + public async viewLpPoolAddLiquidityFees({ + inMarketIndex, + inAmount, + lpPool, + txParams, + }: { + inMarketIndex: number; + inAmount: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(inMarketIndex); + const inMarketMint = spotMarket.mint; + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const lpMint = lpPool.mint; + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.viewLpPoolAddLiquidityFees( + inMarketIndex, + inAmount, + { + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + inMarketMint, + inConstituent, + lpMint, + constituentTargetBase, + }, + remainingAccounts, + } + ); + } + + public async lpPoolRemoveLiquidity({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + txParams, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + }): Promise { + const ixs: TransactionInstruction[] = []; + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [outMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(outMarketIndex); + const outMarketMint = spotMarket.mint; + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + if (outMarketMint.equals(WRAPPED_SOL_MINT)) { + ixs.push( + createAssociatedTokenAccountIdempotentInstruction( + this.wallet.publicKey, + await this.getAssociatedTokenAccount(outMarketIndex, false), + this.wallet.publicKey, + WRAPPED_SOL_MINT + ) + ); + } + const userOutTokenAccount = await this.getAssociatedTokenAccount( + outMarketIndex, + false + ); + const constituentOutTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getAssociatedTokenAddress( + lpMint, + this.wallet.publicKey, + true + ); + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + ixs.push( + this.program.instruction.lpPoolRemoveLiquidity( + outMarketIndex, + lpToBurn, + minAmountOut, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + outMarketMint, + outConstituent, + userOutTokenAccount, + constituentOutTokenAccount, + userLpTokenAccount, + spotMarketTokenAccount: spotMarket.vault, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ) + ); + return ixs; + } + + public async viewLpPoolRemoveLiquidityFees({ + outMarketIndex, + lpToBurn, + lpPool, + txParams, + }: { + outMarketIndex: number; + lpToBurn: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [outMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(outMarketIndex); + const outMarketMint = spotMarket.mint; + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const lpMint = lpPool.mint; + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.viewLpPoolRemoveLiquidityFees( + outMarketIndex, + lpToBurn, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + outMarketMint, + outConstituent, + lpMint, + constituentTargetBase, + }, + } + ); + } + + public async getAllLpPoolAddLiquidityIxs( + { + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + }, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true, + view = false + ): Promise { + const ixs: TransactionInstruction[] = []; + + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs( + lpPool, + constituentMap, + includeUpdateConstituentOracleInfo + )) + ); + + if (view) { + ixs.push( + await this.getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }) + ); + } else { + ixs.push( + ...(await this.getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + })) + ); + } + + return ixs; + } + + public async getAllLpPoolRemoveLiquidityIxs( + { + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + }, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true, + view = false + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push( + ...(await this.getAllSettlePerpToLpPoolIxs( + lpPool.name, + this.getPerpMarketAccounts() + .filter((marketAccount) => marketAccount.lpStatus > 0) + .map((marketAccount) => marketAccount.marketIndex) + )) + ); + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs( + lpPool, + constituentMap, + includeUpdateConstituentOracleInfo + )) + ); + if (view) { + ixs.push( + await this.getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }) + ); + } else { + ixs.push( + ...(await this.getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + })) + ); + } + + return ixs; + } + + public async getAllUpdateLpPoolAumIxs( + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true + ): Promise { + const ixs: TransactionInstruction[] = []; + const constituents: ConstituentAccount[] = Array.from( + constituentMap.values() + ); + + if (includeUpdateConstituentOracleInfo) { + for (const constituent of constituents) { + ixs.push(await this.getUpdateConstituentOracleInfoIx(constituent)); + } + } + + const spotMarketIndexes = constituents.map( + (constituent) => constituent.spotMarketIndex + ); + ixs.push(await this.getUpdateLpPoolAumIxs(lpPool, spotMarketIndexes)); + return ixs; + } + + public async getAllUpdateConstituentTargetBaseIxs( + perpMarketIndexes: number[], + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true + ): Promise { + const ixs: TransactionInstruction[] = []; + + ixs.push(await this.getUpdateAmmCacheIx(perpMarketIndexes)); + + const constituents: ConstituentAccount[] = Array.from( + constituentMap.values() + ); + + if (includeUpdateConstituentOracleInfo) { + for (const constituent of constituents) { + ixs.push(await this.getUpdateConstituentOracleInfoIx(constituent)); + } + } + + ixs.push( + await this.getUpdateLpConstituentTargetBaseIx( + lpPool.name, + Array.from(constituentMap.values()).map( + (constituent) => constituent.pubkey + ) + ) + ); + + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs(lpPool, constituentMap, false)) + ); + + return ixs; + } + + async settlePerpToLpPool( + lpPoolName: number[], + perpMarketIndexes: number[] + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getSettlePerpToLpPoolIx(lpPoolName, perpMarketIndexes), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getSettlePerpToLpPoolIx( + lpPoolName: number[], + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = []; + remainingAccounts.push( + ...perpMarketIndexes.map((index) => { + return { + pubkey: this.getPerpMarketAccount(index).pubkey, + isSigner: false, + isWritable: true, + }; + }) + ); + const quoteSpotMarketAccount = this.getQuoteSpotMarketAccount(); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return this.program.instruction.settlePerpToLpPool({ + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: quoteSpotMarketAccount.pubkey, + constituent: getConstituentPublicKey(this.program.programId, lpPool, 0), + constituentQuoteTokenAccount: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + 0 + ), + lpPool, + quoteTokenVault: quoteSpotMarketAccount.vault, + tokenProgram: this.getTokenProgramForSpotMarket(quoteSpotMarketAccount), + }, + remainingAccounts, + }); + } + + public async getAllSettlePerpToLpPoolIxs( + lpPoolName: number[], + marketIndexes: number[] + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push(await this.getUpdateAmmCacheIx(marketIndexes)); + ixs.push(await this.getSettlePerpToLpPoolIx(lpPoolName, marketIndexes)); + return ixs; + } + + /** + * Below here are the transaction sending functions + */ + private handleSignedTransaction(signedTxs: SignedTxData[]) { if (this.enableMetricsEvents && this.metricsEventEmitter) { this.metricsEventEmitter.emit('txSigned', signedTxs); diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index b3723a2ae8..ebfa4b6954 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'; @@ -19,9 +19,6 @@ import { import { Coder, Program } from '@coral-xyz/anchor'; import { WebSocketAccountSubscriber } from './accounts/webSocketAccountSubscriber'; import { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscriberV2'; -import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; -import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; -import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; export type DriftClientConfig = { connection: Connection; @@ -66,7 +63,6 @@ export type DriftClientSubscriptionConfig = resubTimeoutMs?: number; logResubMessages?: boolean; commitment?: Commitment; - programUserAccountSubscriber?: WebSocketProgramAccountSubscriber; perpMarketAccountSubscriber?: new ( accountName: string, program: Program, @@ -75,17 +71,14 @@ export type DriftClientSubscriptionConfig = resubOpts?: ResubOpts, commitment?: Commitment ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - /** If you use V2 here, whatever you pass for perpMarketAccountSubscriber and oracleAccountSubscriber will be ignored and it will use v2 under the hood regardless */ - driftClientAccountSubscriber?: new ( + oracleAccountSubscriber?: new ( + accountName: string, program: Program, - perpMarketIndexes: number[], - spotMarketIndexes: number[], - oracleInfos: OracleInfo[], - shouldFindAllMarketsAndOracles: boolean, - delistedMarketSetting: DelistedMarketSetting - ) => - | WebSocketDriftClientAccountSubscriber - | WebSocketDriftClientAccountSubscriberV2; + accountPublicKey: PublicKey, + decodeBuffer?: (buffer: Buffer) => any, + resubOpts?: ResubOpts, + commitment?: Commitment + ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; } | { type: 'polling'; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index f923272c05..64bc5d6f40 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4332,6 +4332,11 @@ "isMut": true, "isSigner": false }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, { "name": "oracle", "isMut": false, @@ -4460,6 +4465,58 @@ } ] }, + { + "name": "initializeAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateInitialAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializePredictionMarket", "accounts": [ @@ -4673,6 +4730,97 @@ } ] }, + { + "name": "updatePerpMarketLpPoolPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketLpPoolStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpStatus", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketLpPoolFeeTransferScalar", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "optionalLpFeeTransferScalar", + "type": { + "option": "u8" + } + }, + { + "name": "optionalLpNetPnlTransferScalar", + "type": { + "option": "u8" + } + } + ] + }, { "name": "settleExpiredMarketPoolsToRevenuePool", "accounts": [ @@ -5798,6 +5946,11 @@ "name": "perpMarket", "isMut": true, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -6164,6 +6317,11 @@ "name": "oldOracle", "isMut": false, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -7218,6 +7376,119 @@ } ] }, + { + "name": "initializeLpPool", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "minMintFee", + "type": "i64" + }, + { + "name": "maxAum", + "type": "u128" + }, + { + "name": "maxSettleQuoteAmountPerMarket", + "type": "u64" + }, + { + "name": "whitelistMint", + "type": "publicKey" + } + ] + }, + { + "name": "increaseLpPoolMaxAum", + "accounts": [ + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newMaxAum", + "type": "u128" + } + ] + }, { "name": "updateHighLeverageModeConfig", "accounts": [ @@ -7499,72 +7770,1812 @@ "type": "bool" } ] - } - ], - "accounts": [ + }, { - "name": "OpenbookV2FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { + "name": "updateFeatureBitFlagsSettleLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsSwapLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsMintRedeemLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "initializeConstituent", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentVault", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "maxWeightDeviation", + "type": "i64" + }, + { + "name": "swapFeeMin", + "type": "i64" + }, + { + "name": "swapFeeMax", + "type": "i64" + }, + { + "name": "maxBorrowTokenAmount", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", + "type": "u64" + }, + { + "name": "costToTrade", + "type": "i32" + }, + { + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "derivativeWeight", + "type": "u64" + }, + { + "name": "volatility", + "type": "u64" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "newConstituentCorrelations", + "type": { + "vec": "i64" + } + } + ] + }, + { + "name": "updateConstituentStatus", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "newStatus", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentParams", + "accounts": [ + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "constituentParams", + "type": { + "defined": "ConstituentParams" + } + } + ] + }, + { + "name": "updateLpPoolParams", + "accounts": [ + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolParams", + "type": { + "defined": "LpPoolParams" + } + } + ] + }, + { + "name": "addAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "updateAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "removeAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentCorrelationData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "index1", + "type": "u16" + }, + { + "name": "index2", + "type": "u16" + }, + { + "name": "correlation", + "type": "i64" + } + ] + }, + { + "name": "updateLpConstituentTargetBase", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammConstituentMapping", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateLpPoolAum", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateAmmCache", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "overrideAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "overrideParams", + "type": { + "defined": "OverrideAmmCacheParams" + } + } + ] + }, + { + "name": "lpPoolSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u64" + } + ] + }, + { + "name": "viewLpPoolSwapFees", + "accounts": [ + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "inTargetWeight", + "type": "i64" + }, + { + "name": "outTargetWeight", + "type": "i64" + } + ] + }, + { + "name": "lpPoolAddLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + }, + { + "name": "minMintAmount", + "type": "u64" + } + ] + }, + { + "name": "lpPoolRemoveLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolAddLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolRemoveLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + } + ] + }, + { + "name": "beginLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" + } + ] + }, + { + "name": "endLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentOracleInfo", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositToProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdrawFromProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "settlePerpToLpPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentQuoteTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "AmmCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "cache", + "type": { + "vec": { + "defined": "CacheInfo" + } + } + } + ] + } + }, + { + "name": "OpenbookV2FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { "name": "openbookV2ProgramId", "type": "publicKey" }, { - "name": "openbookV2Market", - "type": "publicKey" + "name": "openbookV2Market", + "type": "publicKey" + }, + { + "name": "openbookV2MarketAuthority", + "type": "publicKey" + }, + { + "name": "openbookV2EventHeap", + "type": "publicKey" + }, + { + "name": "openbookV2Bids", + "type": "publicKey" + }, + { + "name": "openbookV2Asks", + "type": "publicKey" + }, + { + "name": "openbookV2BaseVault", + "type": "publicKey" + }, + { + "name": "openbookV2QuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PhoenixV1FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "phoenixProgramId", + "type": "publicKey" + }, + { + "name": "phoenixLogAuthority", + "type": "publicKey" + }, + { + "name": "phoenixMarket", + "type": "publicKey" + }, + { + "name": "phoenixBaseVault", + "type": "publicKey" + }, + { + "name": "phoenixQuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "SerumV3FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "serumProgramId", + "type": "publicKey" + }, + { + "name": "serumMarket", + "type": "publicKey" + }, + { + "name": "serumRequestQueue", + "type": "publicKey" + }, + { + "name": "serumEventQueue", + "type": "publicKey" + }, + { + "name": "serumBids", + "type": "publicKey" + }, + { + "name": "serumAsks", + "type": "publicKey" + }, + { + "name": "serumBaseVault", + "type": "publicKey" + }, + { + "name": "serumQuoteVault", + "type": "publicKey" + }, + { + "name": "serumOpenOrders", + "type": "publicKey" + }, + { + "name": "serumSignerNonce", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "HighLeverageModeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxUsers", + "type": "u32" + }, + { + "name": "currentUsers", + "type": "u32" + }, + { + "name": "reduceOnly", + "type": "u8" }, { - "name": "openbookV2MarketAuthority", - "type": "publicKey" + "name": "padding1", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "openbookV2EventHeap", - "type": "publicKey" + "name": "currentMaintenanceUsers", + "type": "u32" }, { - "name": "openbookV2Bids", + "name": "padding2", + "type": { + "array": [ + "u8", + 24 + ] + } + } + ] + } + }, + { + "name": "IfRebalanceConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", "type": "publicKey" }, { - "name": "openbookV2Asks", - "type": "publicKey" + "name": "totalInAmount", + "docs": [ + "total amount to be sold" + ], + "type": "u64" }, { - "name": "openbookV2BaseVault", - "type": "publicKey" + "name": "currentInAmount", + "docs": [ + "amount already sold" + ], + "type": "u64" }, { - "name": "openbookV2QuoteVault", - "type": "publicKey" + "name": "currentOutAmount", + "docs": [ + "amount already bought" + ], + "type": "u64" }, { - "name": "marketIndex", + "name": "currentOutAmountTransferred", + "docs": [ + "amount already transferred to revenue pool" + ], + "type": "u64" + }, + { + "name": "currentInAmountSinceLastTransfer", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochStartTs", + "docs": [ + "start time of epoch" + ], + "type": "i64" + }, + { + "name": "epochInAmount", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "docs": [ + "max amount to swap in epoch" + ], + "type": "u64" + }, + { + "name": "epochDuration", + "docs": [ + "duration of epoch" + ], + "type": "i64" + }, + { + "name": "outMarketIndex", + "docs": [ + "market index to sell" + ], "type": "u16" }, { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } + "name": "inMarketIndex", + "docs": [ + "market index to buy" + ], + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" }, { "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } + "type": "u8" }, { - "name": "padding", + "name": "padding2", "type": { "array": [ "u8", - 4 + 32 ] } } @@ -7572,56 +9583,90 @@ } }, { - "name": "PhoenixV1FulfillmentConfig", + "name": "InsuranceFundStake", "type": { "kind": "struct", "fields": [ { - "name": "pubkey", + "name": "authority", "type": "publicKey" }, { - "name": "phoenixProgramId", - "type": "publicKey" + "name": "ifShares", + "type": "u128" }, { - "name": "phoenixLogAuthority", - "type": "publicKey" + "name": "lastWithdrawRequestShares", + "type": "u128" }, { - "name": "phoenixMarket", - "type": "publicKey" + "name": "ifBase", + "type": "u128" }, { - "name": "phoenixBaseVault", - "type": "publicKey" + "name": "lastValidTs", + "type": "i64" }, { - "name": "phoenixQuoteVault", - "type": "publicKey" + "name": "lastWithdrawRequestValue", + "type": "u64" + }, + { + "name": "lastWithdrawRequestTs", + "type": "i64" + }, + { + "name": "costBasis", + "type": "i64" }, { "name": "marketIndex", "type": "u16" }, { - "name": "fulfillmentType", + "name": "padding", "type": { - "defined": "SpotFulfillmentType" + "array": [ + "u8", + 14 + ] } - }, + } + ] + } + }, + { + "name": "ProtocolIfSharesTransferConfig", + "type": { + "kind": "struct", + "fields": [ { - "name": "status", + "name": "whitelistedSigners", "type": { - "defined": "SpotFulfillmentConfigStatus" + "array": [ + "publicKey", + 4 + ] } }, + { + "name": "maxTransferPerEpoch", + "type": "u128" + }, + { + "name": "currentEpochTransfer", + "type": "u128" + }, + { + "name": "nextEpochTs", + "type": "i64" + }, { "name": "padding", "type": { "array": [ - "u8", - 4 + "u128", + 8 ] } } @@ -7629,216 +9674,308 @@ } }, { - "name": "SerumV3FulfillmentConfig", + "name": "LPPool", "type": { "kind": "struct", "fields": [ + { + "name": "name", + "docs": [ + "name of vault, TODO: check type + size" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, { "name": "pubkey", + "docs": [ + "address of the vault." + ], "type": "publicKey" }, { - "name": "serumProgramId", + "name": "mint", "type": "publicKey" }, { - "name": "serumMarket", + "name": "whitelistMint", "type": "publicKey" }, { - "name": "serumRequestQueue", + "name": "constituentTargetBase", + "type": "publicKey" + }, + { + "name": "constituentCorrelations", "type": "publicKey" }, { - "name": "serumEventQueue", - "type": "publicKey" + "name": "maxAum", + "docs": [ + "The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index)", + "which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0)", + "pub quote_constituent_index: u16,", + "QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this" + ], + "type": "u128" + }, + { + "name": "lastAum", + "docs": [ + "QUOTE_PRECISION: AUM of the vault in USD, updated lazily" + ], + "type": "u128" + }, + { + "name": "cumulativeQuoteSentToPerpMarkets", + "docs": [ + "QUOTE PRECISION: Cumulative quotes from settles" + ], + "type": "u128" + }, + { + "name": "cumulativeQuoteReceivedFromPerpMarkets", + "type": "u128" + }, + { + "name": "totalMintRedeemFeesPaid", + "docs": [ + "QUOTE_PRECISION: Total fees paid for minting and redeeming LP tokens" + ], + "type": "i128" + }, + { + "name": "lastAumSlot", + "docs": [ + "timestamp of last AUM slot" + ], + "type": "u64" + }, + { + "name": "maxSettleQuoteAmount", + "type": "u64" + }, + { + "name": "lastHedgeTs", + "docs": [ + "timestamp of last vAMM revenue rebalance" + ], + "type": "u64" }, { - "name": "serumBids", - "type": "publicKey" + "name": "mintRedeemId", + "docs": [ + "Every mint/redeem has a monotonically increasing id. This is the next id to use" + ], + "type": "u64" }, { - "name": "serumAsks", - "type": "publicKey" + "name": "settleId", + "type": "u64" }, { - "name": "serumBaseVault", - "type": "publicKey" + "name": "minMintFee", + "docs": [ + "PERCENTAGE_PRECISION" + ], + "type": "i64" }, { - "name": "serumQuoteVault", - "type": "publicKey" + "name": "tokenSupply", + "type": "u64" }, { - "name": "serumOpenOrders", - "type": "publicKey" + "name": "volatility", + "type": "u64" }, { - "name": "serumSignerNonce", - "type": "u64" + "name": "constituents", + "type": "u16" }, { - "name": "marketIndex", + "name": "quoteConsituentIndex", "type": "u16" }, { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } + "name": "bump", + "type": "u8" }, { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" }, { "name": "padding", - "type": { - "array": [ - "u8", - 4 - ] - } + "type": "u8" } ] } }, { - "name": "HighLeverageModeConfig", + "name": "Constituent", "type": { "kind": "struct", "fields": [ { - "name": "maxUsers", - "type": "u32" + "name": "pubkey", + "docs": [ + "address of the constituent" + ], + "type": "publicKey" }, { - "name": "currentUsers", - "type": "u32" + "name": "mint", + "type": "publicKey" }, { - "name": "reduceOnly", - "type": "u8" + "name": "lpPool", + "type": "publicKey" }, { - "name": "padding1", - "type": { - "array": [ - "u8", - 3 - ] - } + "name": "vault", + "type": "publicKey" }, { - "name": "currentMaintenanceUsers", - "type": "u32" + "name": "totalSwapFees", + "docs": [ + "total fees received by the constituent. Positive = fees received, Negative = fees paid" + ], + "type": "i128" }, { - "name": "padding2", + "name": "spotBalance", + "docs": [ + "spot borrow-lend balance for constituent" + ], "type": { - "array": [ - "u8", - 24 - ] + "defined": "ConstituentSpotBalance" } - } - ] - } - }, - { - "name": "IfRebalanceConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" }, { - "name": "totalInAmount", + "name": "maxWeightDeviation", "docs": [ - "total amount to be sold" + "max deviation from target_weight allowed for the constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentInAmount", + "name": "swapFeeMin", "docs": [ - "amount already sold" + "min fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentOutAmount", + "name": "swapFeeMax", "docs": [ - "amount already bought" + "max fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentOutAmountTransferred", + "name": "maxBorrowTokenAmount", "docs": [ - "amount already transferred to revenue pool" + "Max Borrow amount:", + "precision: token precision" ], "type": "u64" }, { - "name": "currentInAmountSinceLastTransfer", + "name": "vaultTokenBalance", "docs": [ - "amount already bought in epoch" + "ata token balance in token precision" ], "type": "u64" }, { - "name": "epochStartTs", - "docs": [ - "start time of epoch" - ], + "name": "lastOraclePrice", "type": "i64" }, { - "name": "epochInAmount", - "docs": [ - "amount already bought in epoch" - ], + "name": "lastOracleSlot", "type": "u64" }, { - "name": "epochMaxInAmount", - "docs": [ - "max amount to swap in epoch" - ], + "name": "oracleStalenessThreshold", "type": "u64" }, { - "name": "epochDuration", + "name": "flashLoanInitialTokenAmount", + "type": "u64" + }, + { + "name": "nextSwapId", "docs": [ - "duration of epoch" + "Every swap to/from this constituent has a monotonically increasing id. This is the next id to use" ], - "type": "i64" + "type": "u64" }, { - "name": "outMarketIndex", + "name": "derivativeWeight", "docs": [ - "market index to sell" + "percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight" ], - "type": "u16" + "type": "u64" }, { - "name": "inMarketIndex", + "name": "volatility", + "type": "u64" + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "constituentDerivativeIndex", "docs": [ - "market index to buy" + "The `constituent_index` of the parent constituent. -1 if it is a parent index", + "Example: if in a pool with SOL (parent) and dSOL (derivative),", + "SOL.constituent_index = 1, SOL.constituent_derivative_index = -1,", + "dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1" ], + "type": "i16" + }, + { + "name": "spotMarketIndex", "type": "u16" }, { - "name": "maxSlippageBps", + "name": "constituentIndex", "type": "u16" }, { - "name": "swapMode", + "name": "decimals", + "type": "u8" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "vaultBump", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", "type": "u8" }, { @@ -7846,11 +9983,15 @@ "type": "u8" }, { - "name": "padding2", + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ "u8", - 32 + 2 ] } } @@ -7858,92 +9999,98 @@ } }, { - "name": "InsuranceFundStake", + "name": "AmmConstituentMapping", "type": { "kind": "struct", "fields": [ { - "name": "authority", + "name": "lpPool", "type": "publicKey" }, { - "name": "ifShares", - "type": "u128" - }, - { - "name": "lastWithdrawRequestShares", - "type": "u128" - }, - { - "name": "ifBase", - "type": "u128" - }, - { - "name": "lastValidTs", - "type": "i64" - }, - { - "name": "lastWithdrawRequestValue", - "type": "u64" - }, - { - "name": "lastWithdrawRequestTs", - "type": "i64" - }, - { - "name": "costBasis", - "type": "i64" - }, - { - "name": "marketIndex", - "type": "u16" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ "u8", - 14 + 3 ] } + }, + { + "name": "weights", + "type": { + "vec": { + "defined": "AmmConstituentDatum" + } + } } ] } }, { - "name": "ProtocolIfSharesTransferConfig", + "name": "ConstituentTargetBase", "type": { "kind": "struct", "fields": [ { - "name": "whitelistedSigners", + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ - "publicKey", - 4 + "u8", + 3 ] } }, { - "name": "maxTransferPerEpoch", - "type": "u128" - }, + "name": "targets", + "type": { + "vec": { + "defined": "TargetsDatum" + } + } + } + ] + } + }, + { + "name": "ConstituentCorrelations", + "type": { + "kind": "struct", + "fields": [ { - "name": "currentEpochTransfer", - "type": "u128" + "name": "lpPool", + "type": "publicKey" }, { - "name": "nextEpochTs", - "type": "i64" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 8 + "u8", + 3 ] } + }, + { + "name": "correlations", + "type": { + "vec": "i64" + } } ] } @@ -8266,8 +10413,20 @@ "type": "u8" }, { - "name": "padding1", - "type": "u32" + "name": "lpFeeTransferScalar", + "type": "u8" + }, + { + "name": "lpStatus", + "type": "u8" + }, + { + "name": "lpPausedOperations", + "type": "u8" + }, + { + "name": "lpExchangeFeeExcluscionScalar", + "type": "u8" }, { "name": "lastFillPrice", @@ -9012,12 +11171,16 @@ "name": "featureBitFlags", "type": "u8" }, + { + "name": "lpPoolFeatureBitFlags", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 9 + 8 ] } } @@ -9499,103 +11662,386 @@ "type": "publicKey" }, { - "name": "name", - "type": { - "array": [ - "u8", - 32 - ] - } + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "FuelOverflow", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The authority of this overflow account" + ], + "type": "publicKey" + }, + { + "name": "fuelInsurance", + "type": "u128" + }, + { + "name": "fuelDeposits", + "type": "u128" + }, + { + "name": "fuelBorrows", + "type": "u128" + }, + { + "name": "fuelPositions", + "type": "u128" + }, + { + "name": "fuelTaker", + "type": "u128" + }, + { + "name": "fuelMaker", + "type": "u128" + }, + { + "name": "lastFuelSweepTs", + "type": "u32" + }, + { + "name": "lastResetTs", + "type": "u32" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 6 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "OverrideAmmCacheParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteOwedFromLpPool", + "type": { + "option": "i64" + } + }, + { + "name": "lastSettleSlot", + "type": { + "option": "u64" + } + }, + { + "name": "lastFeePoolTokenAmount", + "type": { + "option": "u128" + } + }, + { + "name": "lastNetPnlPoolTokenAmount", + "type": { + "option": "i128" + } + } + ] + } + }, + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } + }, + { + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } + }, + { + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } + }, + { + "name": "excludeTotalLiqFee", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "ConstituentParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxWeightDeviation", + "type": { + "option": "i64" + } + }, + { + "name": "swapFeeMin", + "type": { + "option": "i64" + } + }, + { + "name": "swapFeeMax", + "type": { + "option": "i64" + } + }, + { + "name": "maxBorrowTokenAmount", + "type": { + "option": "u64" + } + }, + { + "name": "oracleStalenessThreshold", + "type": { + "option": "u64" + } + }, + { + "name": "costToTradeBps", + "type": { + "option": "i32" + } + }, + { + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } + }, + { + "name": "derivativeWeight", + "type": { + "option": "u64" + } + }, + { + "name": "volatility", + "type": { + "option": "u64" + } + }, + { + "name": "gammaExecution", + "type": { + "option": "u8" + } + }, + { + "name": "gammaInventory", + "type": { + "option": "u8" + } + }, + { + "name": "xi", + "type": { + "option": "u8" + } + } + ] + } + }, + { + "name": "LpPoolParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxSettleQuoteAmount", + "type": { + "option": "u64" + } + }, + { + "name": "volatility", + "type": { + "option": "u64" + } + }, + { + "name": "gammaExecution", + "type": { + "option": "u8" + } + }, + { + "name": "xi", + "type": { + "option": "u8" + } + }, + { + "name": "whitelistMint", + "type": { + "option": "publicKey" + } + } + ] + } + }, + { + "name": "AddAmmConstituentMappingDatum", + "type": { + "kind": "struct", + "fields": [ + { + "name": "constituentIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "weight", + "type": "i64" } ] } }, { - "name": "FuelOverflow", + "name": "CacheInfo", "type": { "kind": "struct", "fields": [ { - "name": "authority", - "docs": [ - "The authority of this overflow account" - ], + "name": "oracle", "type": "publicKey" }, { - "name": "fuelInsurance", + "name": "lastFeePoolTokenAmount", "type": "u128" }, { - "name": "fuelDeposits", - "type": "u128" + "name": "lastNetPnlPoolTokenAmount", + "type": "i128" }, { - "name": "fuelBorrows", + "name": "lastExchangeFees", "type": "u128" }, { - "name": "fuelPositions", + "name": "lastSettleAmmExFees", "type": "u128" }, { - "name": "fuelTaker", - "type": "u128" + "name": "lastSettleAmmPnl", + "type": "i128" }, { - "name": "fuelMaker", - "type": "u128" + "name": "position", + "docs": [ + "BASE PRECISION" + ], + "type": "i64" }, { - "name": "lastFuelSweepTs", - "type": "u32" + "name": "slot", + "type": "u64" }, { - "name": "lastResetTs", - "type": "u32" + "name": "lastSettleAmount", + "type": "u64" + }, + { + "name": "lastSettleSlot", + "type": "u64" + }, + { + "name": "lastSettleTs", + "type": "i64" + }, + { + "name": "quoteOwedFromLpPool", + "type": "i64" + }, + { + "name": "oraclePrice", + "type": "i64" + }, + { + "name": "oracleSlot", + "type": "u64" + }, + { + "name": "oracleSource", + "type": "u8" + }, + { + "name": "oracleValidity", + "type": "u8" + }, + { + "name": "lpStatusForPerpMarket", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 6 + "u8", + 13 ] } } ] } - } - ], - "types": [ + }, { - "name": "UpdatePerpMarketSummaryStatsParams", + "name": "AmmCacheFixed", "type": { "kind": "struct", "fields": [ { - "name": "quoteAssetAmountWithUnsettledLp", - "type": { - "option": "i64" - } - }, - { - "name": "netUnsettledFundingPnl", - "type": { - "option": "i64" - } + "name": "bump", + "type": "u8" }, { - "name": "updateAmmSummaryStats", + "name": "pad", "type": { - "option": "bool" + "array": [ + "u8", + 3 + ] } }, { - "name": "excludeTotalLiqFee", - "type": { - "option": "bool" - } + "name": "len", + "type": "u32" } ] } @@ -9690,49 +12136,231 @@ "type": "u128" }, { - "name": "ifFee", - "docs": [ - "precision: token mint precision" - ], - "type": "u64" + "name": "ifFee", + "docs": [ + "precision: token mint precision" + ], + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateBorrowForPerpPnlRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liabilityPrice", + "type": "i64" + }, + { + "name": "liabilityTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "LiquidatePerpPnlForDepositRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "assetPrice", + "type": "i64" + }, + { + "name": "assetTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "PerpBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "pnl", + "type": "i128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "clawbackUser", + "type": { + "option": "publicKey" + } + }, + { + "name": "clawbackUserPayment", + "type": { + "option": "u128" + } + }, + { + "name": "cumulativeFundingRateDelta", + "type": "i128" + } + ] + } + }, + { + "name": "SpotBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "borrowAmount", + "type": "u128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "cumulativeDepositInterestDelta", + "type": "u128" + } + ] + } + }, + { + "name": "IfRebalanceConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "totalInAmount", + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "type": "u64" + }, + { + "name": "epochDuration", + "type": "i64" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" + }, + { + "name": "status", + "type": "u8" } ] } }, { - "name": "LiquidateBorrowForPerpPnlRecord", + "name": "ConstituentSpotBalance", "type": { "kind": "struct", "fields": [ { - "name": "perpMarketIndex", - "type": "u16" + "name": "scaledBalance", + "docs": [ + "The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow", + "interest of corresponding market.", + "precision: token precision" + ], + "type": "u128" }, { - "name": "marketOraclePrice", + "name": "cumulativeDeposits", + "docs": [ + "The cumulative deposits/borrows a user has made into a market", + "precision: token mint precision" + ], "type": "i64" }, { - "name": "pnlTransfer", - "type": "u128" - }, - { - "name": "liabilityMarketIndex", + "name": "marketIndex", + "docs": [ + "The market index of the corresponding spot market" + ], "type": "u16" }, { - "name": "liabilityPrice", - "type": "i64" + "name": "balanceType", + "docs": [ + "Whether the position is deposit or borrow" + ], + "type": { + "defined": "SpotBalanceType" + } }, { - "name": "liabilityTransfer", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 5 + ] + } } ] } }, { - "name": "LiquidatePerpPnlForDepositRecord", + "name": "AmmConstituentDatum", "type": { "kind": "struct", "fields": [ @@ -9741,124 +12369,150 @@ "type": "u16" }, { - "name": "marketOraclePrice", - "type": "i64" + "name": "constituentIndex", + "type": "u16" }, { - "name": "pnlTransfer", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "assetMarketIndex", - "type": "u16" + "name": "lastSlot", + "type": "u64" }, { - "name": "assetPrice", + "name": "weight", + "docs": [ + "PERCENTAGE_PRECISION. The weight this constituent has on the perp market" + ], "type": "i64" - }, - { - "name": "assetTransfer", - "type": "u128" } ] } }, { - "name": "PerpBankruptcyRecord", + "name": "AmmConstituentMappingFixed", "type": { "kind": "struct", "fields": [ { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "pnl", - "type": "i128" - }, - { - "name": "ifPayment", - "type": "u128" + "name": "lpPool", + "type": "publicKey" }, { - "name": "clawbackUser", - "type": { - "option": "publicKey" - } + "name": "bump", + "type": "u8" }, { - "name": "clawbackUserPayment", + "name": "pad", "type": { - "option": "u128" + "array": [ + "u8", + 3 + ] } }, { - "name": "cumulativeFundingRateDelta", - "type": "i128" + "name": "len", + "type": "u32" } ] } }, { - "name": "SpotBankruptcyRecord", + "name": "TargetsDatum", "type": { "kind": "struct", "fields": [ { - "name": "marketIndex", - "type": "u16" + "name": "costToTradeBps", + "type": "i32" }, { - "name": "borrowAmount", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "ifPayment", - "type": "u128" + "name": "lastSlot", + "type": "u64" }, { - "name": "cumulativeDepositInterestDelta", - "type": "u128" + "name": "targetBase", + "type": "i64" } ] } }, { - "name": "IfRebalanceConfigParams", + "name": "ConstituentTargetBaseFixed", "type": { "kind": "struct", "fields": [ { - "name": "totalInAmount", - "type": "u64" + "name": "lpPool", + "type": "publicKey" }, { - "name": "epochMaxInAmount", - "type": "u64" + "name": "bump", + "type": "u8" }, { - "name": "epochDuration", - "type": "i64" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "outMarketIndex", - "type": "u16" - }, + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" + } + ] + } + }, + { + "name": "ConstituentCorrelationsFixed", + "type": { + "kind": "struct", + "fields": [ { - "name": "inMarketIndex", - "type": "u16" + "name": "lpPool", + "type": "publicKey" }, { - "name": "maxSlippageBps", - "type": "u16" + "name": "bump", + "type": "u8" }, { - "name": "swapMode", - "type": "u8" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "status", - "type": "u8" + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" } ] } @@ -11872,6 +14526,23 @@ ] } }, + { + "name": "SettlementDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ToLpPool" + }, + { + "name": "FromLpPool" + }, + { + "name": "None" + } + ] + } + }, { "name": "MarginRequirementType", "type": { @@ -11955,6 +14626,15 @@ }, { "name": "UseMMOraclePrice" + }, + { + "name": "UpdateLpConstituentTargetBase" + }, + { + "name": "UpdateLpPoolAum" + }, + { + "name": "LpPoolSwap" } ] } @@ -12285,6 +14965,40 @@ ] } }, + { + "name": "ConstituentStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ReduceOnly" + }, + { + "name": "Decommissioned" + } + ] + } + }, + { + "name": "WeightValidationFlags", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NONE" + }, + { + "name": "EnforceTotalWeight100" + }, + { + "name": "NoNegativeWeights" + }, + { + "name": "NoOverweight" + } + ] + } + }, { "name": "MarginCalculationMode", "type": { @@ -12505,6 +15219,37 @@ ] } }, + { + "name": "PerpLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TrackAmmRevenue" + }, + { + "name": "SettleQuoteOwed" + } + ] + } + }, + { + "name": "ConstituentLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Swap" + }, + { + "name": "Deposit" + }, + { + "name": "Withdraw" + } + ] + } + }, { "name": "MarketStatus", "type": { @@ -12529,13 +15274,30 @@ "name": "WithdrawPaused" }, { - "name": "ReduceOnly" + "name": "ReduceOnly" + }, + { + "name": "Settlement" + }, + { + "name": "Delisted" + } + ] + } + }, + { + "name": "LpStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Uncollateralized" }, { - "name": "Settlement" + "name": "Active" }, { - "name": "Delisted" + "name": "Decommissioning" } ] } @@ -12725,6 +15487,23 @@ ] } }, + { + "name": "LpPoolFeatureBitFlags", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SettleLpPool" + }, + { + "name": "SwapLpPool" + }, + { + "name": "MintRedeemLpPool" + } + ] + } + }, { "name": "UserStatus", "type": { @@ -14072,24 +16851,261 @@ "index": false }, { - "name": "outFundVaultAmountAfter", - "type": "u64", + "name": "outFundVaultAmountAfter", + "type": "u64", + "index": false + }, + { + "name": "inMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "outMarketIndex", + "type": "u16", + "index": false + } + ] + }, + { + "name": "TransferProtocolIfSharesToRevenuePoolRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + }, + { + "name": "shares", + "type": "u128", + "index": false + }, + { + "name": "ifVaultAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "protocolSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "transferAmount", + "type": "u64", + "index": false + } + ] + }, + { + "name": "SwapRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "amountOut", + "type": "u64", + "index": false + }, + { + "name": "amountIn", + "type": "u64", + "index": false + }, + { + "name": "outMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "inMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "outOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "inOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "fee", + "type": "u64", + "index": false + } + ] + }, + { + "name": "SpotMarketVaultDepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "depositBalance", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterestBefore", + "type": "u128", + "index": false + }, + { + "name": "cumulativeDepositInterestAfter", + "type": "u128", + "index": false + }, + { + "name": "depositTokenAmountBefore", + "type": "u64", + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + } + ] + }, + { + "name": "DeleteUserRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "subAccountId", + "type": "u16", + "index": false + }, + { + "name": "keeper", + "type": { + "option": "publicKey" + }, + "index": false + } + ] + }, + { + "name": "FuelSweepRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "authority", + "type": "publicKey", + "index": false + }, + { + "name": "userStatsFuelInsurance", + "type": "u32", + "index": false + }, + { + "name": "userStatsFuelDeposits", + "type": "u32", + "index": false + }, + { + "name": "userStatsFuelBorrows", + "type": "u32", + "index": false + }, + { + "name": "userStatsFuelPositions", + "type": "u32", + "index": false + }, + { + "name": "userStatsFuelTaker", + "type": "u32", + "index": false + }, + { + "name": "userStatsFuelMaker", + "type": "u32", + "index": false + }, + { + "name": "fuelOverflowFuelInsurance", + "type": "u128", + "index": false + }, + { + "name": "fuelOverflowFuelDeposits", + "type": "u128", + "index": false + }, + { + "name": "fuelOverflowFuelBorrows", + "type": "u128", + "index": false + }, + { + "name": "fuelOverflowFuelPositions", + "type": "u128", "index": false }, { - "name": "inMarketIndex", - "type": "u16", + "name": "fuelOverflowFuelTaker", + "type": "u128", "index": false }, { - "name": "outMarketIndex", - "type": "u16", + "name": "fuelOverflowFuelMaker", + "type": "u128", "index": false } ] }, { - "name": "TransferProtocolIfSharesToRevenuePoolRecord", + "name": "FuelSeasonRecord", "fields": [ { "name": "ts", @@ -14097,89 +17113,109 @@ "index": false }, { - "name": "marketIndex", - "type": "u16", + "name": "authority", + "type": "publicKey", "index": false }, { - "name": "amount", - "type": "u64", + "name": "fuelInsurance", + "type": "u128", "index": false }, { - "name": "shares", + "name": "fuelDeposits", "type": "u128", "index": false }, { - "name": "ifVaultAmountBefore", - "type": "u64", + "name": "fuelBorrows", + "type": "u128", "index": false }, { - "name": "protocolSharesBefore", + "name": "fuelPositions", "type": "u128", "index": false }, { - "name": "transferAmount", - "type": "u64", + "name": "fuelTaker", + "type": "u128", + "index": false + }, + { + "name": "fuelMaker", + "type": "u128", + "index": false + }, + { + "name": "fuelTotal", + "type": "u128", "index": false } ] }, { - "name": "SwapRecord", + "name": "LPSettleRecord", "fields": [ { - "name": "ts", - "type": "i64", + "name": "recordId", + "type": "u64", "index": false }, { - "name": "user", - "type": "publicKey", + "name": "lastTs", + "type": "i64", "index": false }, { - "name": "amountOut", + "name": "lastSlot", "type": "u64", "index": false }, { - "name": "amountIn", + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "slot", "type": "u64", "index": false }, { - "name": "outMarketIndex", + "name": "perpMarketIndex", "type": "u16", "index": false }, { - "name": "inMarketIndex", - "type": "u16", + "name": "settleToLpAmount", + "type": "i64", "index": false }, { - "name": "outOraclePrice", + "name": "perpAmmPnlDelta", "type": "i64", "index": false }, { - "name": "inOraclePrice", + "name": "perpAmmExFeeDelta", "type": "i64", "index": false }, { - "name": "fee", - "type": "u64", + "name": "lpAum", + "type": "u128", + "index": false + }, + { + "name": "lpPrice", + "type": "u128", "index": false } ] }, { - "name": "SpotMarketVaultDepositRecord", + "name": "LPSwapRecord", "fields": [ { "name": "ts", @@ -14187,190 +17223,198 @@ "index": false }, { - "name": "marketIndex", - "type": "u16", + "name": "slot", + "type": "u64", "index": false }, { - "name": "depositBalance", - "type": "u128", + "name": "authority", + "type": "publicKey", "index": false }, { - "name": "cumulativeDepositInterestBefore", + "name": "outAmount", "type": "u128", "index": false }, { - "name": "cumulativeDepositInterestAfter", + "name": "inAmount", "type": "u128", "index": false }, { - "name": "depositTokenAmountBefore", - "type": "u64", + "name": "outFee", + "type": "i128", "index": false }, { - "name": "amount", - "type": "u64", + "name": "inFee", + "type": "i128", "index": false - } - ] - }, - { - "name": "DeleteUserRecord", - "fields": [ + }, { - "name": "ts", - "type": "i64", + "name": "outSpotMarketIndex", + "type": "u16", "index": false }, { - "name": "userAuthority", - "type": "publicKey", + "name": "inSpotMarketIndex", + "type": "u16", "index": false }, { - "name": "user", - "type": "publicKey", + "name": "outConstituentIndex", + "type": "u16", "index": false }, { - "name": "subAccountId", + "name": "inConstituentIndex", "type": "u16", "index": false }, { - "name": "keeper", - "type": { - "option": "publicKey" - }, + "name": "outOraclePrice", + "type": "i64", "index": false - } - ] - }, - { - "name": "FuelSweepRecord", - "fields": [ + }, { - "name": "ts", + "name": "inOraclePrice", "type": "i64", "index": false }, { - "name": "authority", - "type": "publicKey", + "name": "lastAum", + "type": "u128", "index": false }, { - "name": "userStatsFuelInsurance", - "type": "u32", + "name": "lastAumSlot", + "type": "u64", "index": false }, { - "name": "userStatsFuelDeposits", - "type": "u32", + "name": "inMarketCurrentWeight", + "type": "i64", "index": false }, { - "name": "userStatsFuelBorrows", - "type": "u32", + "name": "outMarketCurrentWeight", + "type": "i64", "index": false }, { - "name": "userStatsFuelPositions", - "type": "u32", + "name": "inMarketTargetWeight", + "type": "i64", "index": false }, { - "name": "userStatsFuelTaker", - "type": "u32", + "name": "outMarketTargetWeight", + "type": "i64", "index": false }, { - "name": "userStatsFuelMaker", - "type": "u32", + "name": "inSwapId", + "type": "u64", "index": false }, { - "name": "fuelOverflowFuelInsurance", - "type": "u128", + "name": "outSwapId", + "type": "u64", + "index": false + } + ] + }, + { + "name": "LPMintRedeemRecord", + "fields": [ + { + "name": "ts", + "type": "i64", "index": false }, { - "name": "fuelOverflowFuelDeposits", - "type": "u128", + "name": "slot", + "type": "u64", "index": false }, { - "name": "fuelOverflowFuelBorrows", - "type": "u128", + "name": "authority", + "type": "publicKey", "index": false }, { - "name": "fuelOverflowFuelPositions", - "type": "u128", + "name": "description", + "type": "u8", "index": false }, { - "name": "fuelOverflowFuelTaker", + "name": "amount", "type": "u128", "index": false }, { - "name": "fuelOverflowFuelMaker", - "type": "u128", + "name": "fee", + "type": "i128", "index": false - } - ] - }, - { - "name": "FuelSeasonRecord", - "fields": [ + }, { - "name": "ts", + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "constituentIndex", + "type": "u16", + "index": false + }, + { + "name": "oraclePrice", "type": "i64", "index": false }, { - "name": "authority", + "name": "mint", "type": "publicKey", "index": false }, { - "name": "fuelInsurance", - "type": "u128", + "name": "lpAmount", + "type": "u64", "index": false }, { - "name": "fuelDeposits", - "type": "u128", + "name": "lpFee", + "type": "i64", "index": false }, { - "name": "fuelBorrows", + "name": "lpPrice", "type": "u128", "index": false }, { - "name": "fuelPositions", - "type": "u128", + "name": "mintRedeemId", + "type": "u64", "index": false }, { - "name": "fuelTaker", + "name": "lastAum", "type": "u128", "index": false }, { - "name": "fuelMaker", - "type": "u128", + "name": "lastAumSlot", + "type": "u64", "index": false }, { - "name": "fuelTotal", - "type": "u128", + "name": "inMarketCurrentWeight", + "type": "i64", + "index": false + }, + { + "name": "inMarketTargetWeight", + "type": "i64", "index": false } ] @@ -15961,6 +19005,111 @@ "code": 6316, "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" + }, + { + "code": 6317, + "name": "InvalidConstituent", + "msg": "Invalid Constituent" + }, + { + "code": 6318, + "name": "InvalidAmmConstituentMappingArgument", + "msg": "Invalid Amm Constituent Mapping argument" + }, + { + "code": 6319, + "name": "InvalidUpdateConstituentTargetBaseArgument", + "msg": "Invalid update constituent update target weights argument" + }, + { + "code": 6320, + "name": "ConstituentNotFound", + "msg": "Constituent not found" + }, + { + "code": 6321, + "name": "ConstituentCouldNotLoad", + "msg": "Constituent could not load" + }, + { + "code": 6322, + "name": "ConstituentWrongMutability", + "msg": "Constituent wrong mutability" + }, + { + "code": 6323, + "name": "WrongNumberOfConstituents", + "msg": "Wrong number of constituents passed to instruction" + }, + { + "code": 6324, + "name": "OracleTooStaleForLPAUMUpdate", + "msg": "Oracle too stale for LP AUM update" + }, + { + "code": 6325, + "name": "InsufficientConstituentTokenBalance", + "msg": "Insufficient constituent token balance" + }, + { + "code": 6326, + "name": "AMMCacheStale", + "msg": "Amm Cache data too stale" + }, + { + "code": 6327, + "name": "LpPoolAumDelayed", + "msg": "LP Pool AUM not updated recently" + }, + { + "code": 6328, + "name": "ConstituentOracleStale", + "msg": "Constituent oracle is stale" + }, + { + "code": 6329, + "name": "LpInvariantFailed", + "msg": "LP Invariant failed" + }, + { + "code": 6330, + "name": "InvalidConstituentDerivativeWeights", + "msg": "Invalid constituent derivative weights" + }, + { + "code": 6331, + "name": "UnauthorizedDlpAuthority", + "msg": "Unauthorized dlp authority" + }, + { + "code": 6332, + "name": "MaxDlpAumBreached", + "msg": "Max DLP AUM Breached" + }, + { + "code": 6333, + "name": "SettleLpPoolDisabled", + "msg": "Settle Lp Pool Disabled" + }, + { + "code": 6334, + "name": "MintRedeemLpPoolDisabled", + "msg": "Mint/Redeem Lp Pool Disabled" + }, + { + "code": 6335, + "name": "LpPoolSettleInvariantBreached", + "msg": "Settlement amount exceeded" + }, + { + "code": 6336, + "name": "InvalidConstituentOperation", + "msg": "Invalid constituent operation" + }, + { + "code": 6337, + "name": "Unauthorized", + "msg": "Unauthorized for operation" } ], "metadata": { diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 7f30e2afa0..61d803a92c 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'; @@ -136,5 +132,6 @@ export * from './clock/clockSubscriber'; export * from './math/userStatus'; export * from './indicative-quotes/indicativeQuotesSender'; export * from './constants'; +export * from './constituentMap/constituentMap'; export { BN, PublicKey, pyth }; diff --git a/sdk/src/memcmp.ts b/sdk/src/memcmp.ts index 896971ebbf..895c0a839b 100644 --- a/sdk/src/memcmp.ts +++ b/sdk/src/memcmp.ts @@ -1,4 +1,4 @@ -import { MemcmpFilter } from '@solana/web3.js'; +import { MemcmpFilter, PublicKey } from '@solana/web3.js'; import bs58 from 'bs58'; import { BorshAccountsCoder } from '@coral-xyz/anchor'; import { encodeName } from './userName'; @@ -129,3 +129,25 @@ export function getSpotMarketAccountsFilter(): MemcmpFilter { }, }; } + +export function getConstituentFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('Constituent') + ), + }, + }; +} + +export function getConstituentLpPoolFilter( + lpPoolPublicKey: PublicKey +): MemcmpFilter { + return { + memcmp: { + offset: 72, + bytes: lpPoolPublicKey.toBase58(), + }, + }; +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 369520dabf..763ca15cd8 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -741,6 +741,54 @@ export type TransferProtocolIfSharesToRevenuePoolRecord = { transferAmount: BN; }; +export type LPSwapRecord = { + ts: BN; + slot: BN; + authority: PublicKey; + outAmount: BN; + inAmount: BN; + outFee: BN; + inFee: BN; + outSpotMarketIndex: number; + inSpotMarketIndex: number; + outConstituentIndex: number; + inConstituentIndex: number; + outOraclePrice: BN; + inOraclePrice: BN; + outMint: PublicKey; + inMint: PublicKey; + lastAum: BN; + lastAumSlot: BN; + inMarketCurrentWeight: BN; + outMarketCurrentWeight: BN; + inMarketTargetWeight: BN; + outMarketTargetWeight: BN; + inSwapId: BN; + outSwapId: BN; +}; + +export type LPMintRedeemRecord = { + ts: BN; + slot: BN; + authority: PublicKey; + isMinting: boolean; + amount: BN; + fee: BN; + spotMarketIndex: number; + constituentIndex: number; + oraclePrice: BN; + mint: PublicKey; + lpMint: PublicKey; + lpAmount: BN; + lpFee: BN; + lpPrice: BN; + mintRedeemId: BN; + lastAum: BN; + lastAumSlot: BN; + inMarketCurrentWeight: BN; + inMarketTargetWeight: BN; +}; + export type StateAccount = { admin: PublicKey; exchangeStatus: number; @@ -814,6 +862,10 @@ export type PerpMarketAccount = { protectedMakerLimitPriceDivisor: number; protectedMakerDynamicDivisor: number; lastFillPrice: BN; + + lpFeeTransferScalar: number; + lpExchangeFeeExcluscionScalar: number; + lpStatus: number; }; export type HistoricalOracleData = { @@ -1574,3 +1626,135 @@ export type SignedMsgUserOrdersAccount = { authorityPubkey: PublicKey; signedMsgOrderData: SignedMsgOrderId[]; }; + +export type AddAmmConstituentMappingDatum = { + constituentIndex: number; + perpMarketIndex: number; + weight: BN; +}; + +export type AmmConstituentDatum = AddAmmConstituentMappingDatum & { + lastSlot: BN; +}; + +export type AmmConstituentMapping = { + bump: number; + weights: AmmConstituentDatum[]; +}; + +export type TargetDatum = { + costToTradeBps: number; + beta: number; + targetBase: BN; + lastSlot: BN; +}; + +export type ConstituentTargetBaseAccount = { + bump: number; + targets: TargetDatum[]; +}; + +export type LPPoolAccount = { + name: number[]; + pubkey: PublicKey; + mint: PublicKey; + maxAum: BN; + lastAum: BN; + lastAumSlot: BN; + lastAumTs: BN; + lastHedgeTs: BN; + bump: number; + totalMintRedeemFeesPaid: BN; + cumulativeQuoteSentToPerpMarkets: BN; + cumulativeQuoteReceivedFromPerpMarkets: BN; + constituents: number; + whitelistMint: PublicKey; +}; + +export type ConstituentSpotBalance = { + scaledBalance: BN; + cumulativeDeposits: BN; + marketIndex: number; + balanceType: SpotBalanceType; +}; + +export type InitializeConstituentParams = { + spotMarketIndex: number; + decimals: number; + maxWeightDeviation: BN; + swapFeeMin: BN; + swapFeeMax: BN; + maxBorrowTokenAmount: BN; + oracleStalenessThreshold: BN; + costToTrade: number; + derivativeWeight: BN; + constituentDerivativeIndex?: number; + constituentDerivativeDepegThreshold?: BN; + constituentCorrelations: BN[]; + volatility: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; +}; + +export enum ConstituentStatus { + ACTIVE = 0, + REDUCE_ONLY = 1, + DECOMMISSIONED = 2, +} +export enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + +export type ConstituentAccount = { + pubkey: PublicKey; + spotMarketIndex: number; + constituentIndex: number; + decimals: number; + bump: number; + constituentDerivativeIndex: number; + maxWeightDeviation: BN; + maxBorrowTokenAmount: BN; + swapFeeMin: BN; + swapFeeMax: BN; + totalSwapFees: BN; + vaultTokenBalance: BN; + spotBalance: ConstituentSpotBalance; + lastOraclePrice: BN; + lastOracleSlot: BN; + mint: PublicKey; + oracleStalenessThreshold: BN; + lpPool: PublicKey; + vault: PublicKey; + nextSwapId: BN; + derivativeWeight: BN; + flashLoanInitialTokenAmount: BN; + status: number; + pausedOperations: number; +}; + +export type CacheInfo = { + slot: BN; + position: BN; + lastOraclePriceTwap: BN; + oracle: PublicKey; + oracleSource: number; + oraclePrice: BN; + oracleSlot: BN; + lastExchangeFees: BN; + lastFeePoolTokenAmount: BN; + lastNetPnlPoolTokenAmount: BN; + lastSettleAmount: BN; + lastSettleSlot: BN; + lastSettleTs: BN; + lastSettleAmmPnl: BN; + lastSettleAmmExFees: BN; + quoteOwedFromLpPool: BN; + lpStatusForPerpMarket: number; +}; + +export type AmmCache = { + cache: CacheInfo[]; +}; diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index e8ef72b4ea..fc7f0ace2c 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -42,6 +42,8 @@ test_files=( liquidatePerpPnlForDeposit.ts liquidateSpot.ts liquidateSpotSocialLoss.ts + lpPool.ts + lpPoolSwap.ts marketOrder.ts marketOrderBaseAssetAmount.ts maxDeposit.ts @@ -93,4 +95,4 @@ test_files=( for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} || exit 1 -done \ No newline at end of file +done diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index b75e2f22a9..f3fb157085 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,7 +6,10 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(admin.ts) +test_files=( + lpPool.ts + lpPoolSwap.ts +) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/fixtures/token_2022.so b/tests/fixtures/token_2022.so new file mode 100755 index 0000000000000000000000000000000000000000..23c12ecb2fa59e69d92e9a2100b7e0395603d708 GIT binary patch literal 1382016 zcmeFa3xHi!bvJ${$w>yPU&4f&gux;AX7VC>>kxto@xg@fh$(6yxh7zz&SZ#r&}uG& zknL&94M9QL&jwIITHD;2yinVw4-39}WV2H9g(}K(RU(*YM+TUs^1b$-f*V< zilY8#RWwcho)L8_EFgC4c$a3cQ5-_=dIc6@j z@EZE>pcget{Lxr${#5#Jo)tyIT^&*50*k+V+&l|2S5!;p63;mLag7Q?Qh>O5LZBzb;kS6VmZeU(I9jQGqf5PLhQMk0iq~9#?Pe_4VNsdU?eu?VCc! zKdB!|?5nWA_;P_}i~hHGfRCeZC>)wO_{YQ@5TH*qy(5|@gVyOLF}2FH#SvJqS|lS@ z7xhU<=(upH`a_G=iz?HZ>mdv#al^y(~6K5LEN7p}5$ma88Y_FH(h zh1Xg5Qid07V7Pa^`k-64{?b3Sv-pP5!5;v!Cmr9HXvVNd zGKdR%=}#;VHYi+nynw#RQppK_AW_$V@Q)Dv|s*@8aFbGaXKgwDDS=Jc4+xX88N!X5d~*q8gc;- zGHxInMJb)nCY2g%6in;slb=rK#`%oT(>ayrYncu_KL41|vA#p;oOG{KKh$?yc!P!a zSa^$t*)O1b#?^XCjyDkwb;2iejLvnI=kg`gSzjFO(qcRwe9)Z1qgoujPSaDo(((*% zlUJcmyy9q(<=rd|6bC$snFedgV)85GG~!1VYkZWiD~1=!f2nu4|IJm zmCqYL{cRl_$8W0K8%9@I`nNS5<-vj8KOLXghTr}2FO2mQuJ>CCSLwj)C-Fy%8Xu0# zQ{UxA<2Fs!cF*`f&2d+W&eQQ&=v*3=q>g-lR62_?W*DF4pKveB3-_3u>OEH@T#rfR z)Hw!sG5H|ePapMQenYBwx0dHoz7AE{V8=JrH_o^G_Y}3j>^xZ%ynUXIr9E)TTbuRB z6yQ$yp5q_&U$&+>FM@9|&2yvm#cDp=F-buP7Zy}wsa=$3>+3^gzWbycW!NbQ(9DQ2pjedM0Cnp}2 zx!^xy-hEi`LBG8DOB^q)^X|hs9{)4v-SegG*?AW|4*cHsH_h??!_2##Qa;N2aE9(a z9{wZeUC?>FdH1W2f}f5nos;eZ>WBIx7Cvg>6Ba&ZVfM@9^Dg0?PeYyM`TQE{tnc*a zUBfG#cMb1!-X&f?3iIyG7*BO!E^(0iyU{*e zgBCV=ckj?J#&PSsThsW{pLg$6_=)G;{gyu2yn8?4+UDIo8Xu1AR)4&CcV1@PVcx}f z#Ju};k)`?h=0{=PeO&PV(VutMX@8A3?{l@Ai{l+U8xuN5)Nl z-UU9G?;MZk3LdWKH0NEGJNdlJdR(48oq0Ep$M?@Uy?OUrQ=9W{y3Sq7dA>6Bd=(fy zcdS+4$4{z9nqD*BdKU^C92RMB0g4ss*gU!diOEO5l6I7 zKp&B=RJr$Sk}&alm+REiS??aV^8O@4_vXC2TvBBneTS`QO&^;fsxIVjvW#*rxktfT z}- z&a131j&9K6lD7y#;!u8eJrYOTH9k0A>AY%qr}HZDh@&BePoD95_9r!eejSjPUdiEDEUh0~x^Q!7SqZSsXDw;e%zoMUhLHHrmNw22Z629=T#aI!sv005A}!DPr7ULm))s8+FgFEIj$-q zH8I}8DANm5mPU1+*o8fmvd_WJs??{^DFU6?jt+AKUT$iG~XknJ0HyHocB_92Fq3~}yubN%+>91$m zPoc#1Y|>3W2ur!H1s?M!!DE5o;dBlUTK*#}cd~gkd5f**eI0xAFaqE2Bsr`5Q^X)8!pLo8VXX%s8w?)FG^R3&DBaIJ7s3*7{?)F&7 zg-bHy4(nKqN33IE4@CPg|C+1)D6C`q1?T2Gd!C~1>ngP8S(#`5!|lf>l)uJX$L@!m zxsg4B_2ZEIL_c0GjZ5d-XKX)ygnZw&jx~J7nQwtl7LOHzhts>cj%B%%&9|Y>dR(48 zopo#;k6yvU<^NOJkLQV;80zKvu0b!_EKDLKjVjZyTO#w4(77mnLFQ<<*tiIEug?YD zF>2TkWwC|F5#f-2t%P0gYV<0Y?*Ht`*|C3B46qKjS+Zl`ZtIA3{5>SUu>ve~V7C=M z&*~ulW%4hU(>rj5j;S2K9n%#ML(lo zYdJjb5LLV!!!xbN&h-QkL0q~Y*~M^0=m+Qf$jx-t@AUX@LxSSO)n7F2r5v|1 z9D#Xxulx`fM@99$y)M_nDDmmpO1bb5hk4|B=*(+0o613yRAn(y<37eWHqr0dO+tFP zGo&2AH*RNodCR{leqogORysCm`phesK0~%)0iSFwYJ}|DD13Q{+Jmh3HT=>c+n3DI zEf}2}?3T#sTb{#xLSB{zl}xw;S2f!duKQXsgjbUuAtINf4K8r5_Gjxo0yxe=hx4 zrLM^18SLjcGPFH7nSgpjz89T@HzN(q-%}Fhehg%{%`$tHN&agFgu91uV*uI%RFKITkY-F zugtS4Xkxus+oB z?|e1W7fpYj(kqOy{DI{>57ZcAI`oUjr9DYE`OVMGKraSGlIzGmiBMd?3C!&>6TPWO>)9+p78?$;Gb}rMyouCYW$6OueZ_9xQ31E`93a1uYg1OXeaQ1UzW3h zaE@063YWNsjn_7LvW@yt96IxgA4b|-eeHrd1Ul%LjE zIYuYs`-^W1UYwSr#$2X1`GVnR!e=j#Cg=IAv5)0#7vh2Gl$dxl;U^_#@pv(DJ=J;Da7U`lFi8`fm~W02bOTzUCLdhV*6sPW*(QfMOhRK!@!@ z5OHBhy=Y+WH2snC1BZ*smp4mYKK{cH`NH+bkrMO6ej{L!?@Pu}ndMn}d%K92<9(&z zdt7piEHipac@ib~nmnJd9Ih|2^ietpGN(hwr^2gC>IXdql?eEq%Hao!g&nPQO!>RY z0iM$HHUSow9cM=++{|xu-K$_N=LNqH{UI|xzgiGEYU9!Om7R|ZQ-H0MdpXN1#4oTg z@vOuYG?l4qG`_J|J)!ev{qYcpF0MKM4QtrMh5iN~*)$!^Lv=NcR{rvJLW5{TjonO} z-$&o|C+H#4MNxS-{)&7fx={s(^TaiXX4EKZFuH?Y^jeDtK9)u6Ef20!h!**4d4O*+ z$bieGv|pB5{H5lb9_D%_l*FOR2r=3p(EN@;Ai`Y*Axfz$={> zE@zytQ|FtVBc+!lE4?-uy$CM%r;J`#SpJ3d?*Ma(d#%L-pB8#uzd+N|`$z_lNbTRt zG+FQ4L^b~Yuf=zgdCk_Hu)l%R8wKKw^*!2Mn9kNG+ZdlXeU0B^9p#d5l5&zo?fKCI zNr~}34*i^NVbdVTD!$o1v&rHt_TWU~+%F_o$FHKWUC=iBWvu8>~F1R$<)rFyD2 z;1|X(Qn0T2#K%AWam0x?wLC(2AK!~+bVNcb)hpn;d|4v-z!?Rx$HIl{Nw+1E9r1~s z!%o*9k{Kl@>uudpM=shMPe5)Rw6MTo-GK3QgZRGwgdGjA+1$=^%7*-Mg} zS&r}bKYMh>>QL$#V)>KN8FT|hppU6f^wWDiM}+>;yeDrFAM;P41JsrBhmHT7eyeT$ zCi#_qt51)9-cO*g!Zf41@oyX73VTadS+AZR-9hiOPm|s^ zKMi`P^jgbS&oF*?h-6aw;HTSLoL-v+f5cZ}%83c-eE|z{z2EcyDA4uu-Tl+IV8l;*sb zm&d+e+$M*onmn-eOR~D1ZYcS5>E`_dxp4Ps((hlsG5LHJY(Fquq|J6UW z1DGuLocSZ!NFVwj;D8tAA-9``4NRA5i~S&aw3erTrC&_1c6%f2n@Eo%p2rA9t^mT~ zq83zLvXuT(t|zMtmT7pTWd2^pk5mbl3>7s!>0mj@)q5;n909KLbUpgPa2=#nDx0j+ECK!DO+m<2+pn zZ|m5g_wToL+!ysqyzDo3ED#^%eD^0>%h@gQakSRTnW_8D+d7u%P2FuBx9G{~LdV0= zg_i%l8(Q;Yzd4RxZ26z3Hll4ES4B`cw{^TT!g{mN@w|?+EdK@TTJ!g5{tGRCN%f0u z9Y3QxEQO9=*ZpkZeZe)Y`Ouy?f)xez6m@03t>cF1`4TR4d@w?fA>Z~_w&uH5;^SzM z<(nVu4v7N6Q6RQ(ejhXX-FV_#Gj~G7{ep3V!SE1vi zzCZ!^SvmS+i4sSD2XKMg7+tLX&+CnL!2cje|GAPLM~?wq;IED@RsUzBwc-Q*UVCc< z=@^Hr#E+vdBPj5%(v@?e<4qAvc7R`*8!yEF3kE+B{V&aTPBvC4B&q@H=egB{HF~5&mrn2$({D}F|1Oswd>%(hn!YiYzohB6r0MU<@dH+>aWs;qeBn>F+cbT9n*OIb`f~mmPSdZ;(HHU}j$WIlpPg$z>HC^Ay>Hy| zH>c_E%+Y7QmcKDg|K)M{=v8U@$8!9Gc|VSRB29lRm!JJ}Wtu)GM_Y3so?VUYMr;ZjOH_f1jVG zUz5X+^VgCzy^_n1o#I%vB)0F3Ir^Ywu}WR0hn(CX{lUddKPM+IC}%-srccfF4|4i` z0LK59a``DAW~TW+k*j~U=KrxYeJGb7^55-%DF3Ql{g{{iJR#CA%kkGbNq4>b9a@Xb zb5qaMUw#fNU=WF$C;ImEzxwuL`2CM>{H4F{86@085|*N6jy5{TxE4@%HLu_8^Vk<= zP6esC9WvGzm1Moq*Qp7pZ{DQ?A4cGBfBH|pG%)zjPvUpzn*~qt`MvJ%zXHGaec-uE z1_u98(}53MxDdTVxF3AvyQG(5x)(O<*!ya^p88> z85S_qfG`k;_1&+d9&3d6f0=4`x<8)U`6>RizF)tUWgx55w*e*=m#lw8>6mmMS0DU3 zgGoxSa)A|MJ&vA1J5V(E_q?ym`uc7H!)^dNC*2RHwl08xMUQzztVt$^ketsw%Wcw<%pIN>4kmP{fobqdis6dA$ zJVP3NozWL9_WdZ|PcDyeUjhADko2AR$RBZwrktnx7Vrao-q5SjdOrF!um1NL`Er0~ z#tA1Hfc@@xzH}Db*|7eBU24ncg-GU5{spr4hW28fNiL8G_--$;{>|P8MGO6%q<1K^ z(AU~ApV`~S#Y0l%kF^m4C?w5WXJ1_gBfNzVx^Vtzk|QHhsnJ&jg!~@t z-JNhFD-I)+@8Kromip{`)yTC)4JRaTlw3@>#O^mH=TI*Cx!U08n9hL~C*_n_esT^Q z;^)MZbI!MP^`hh)U@P)Z+QXA%8kG02)ED}FfO12|hk&@N`8!B{o*&p`^ZWjt^T~ki zqo5yJ%IRZ~g(;tZa*g~<78>4m9zOvyi;JV9sr=fIJ8$^tzX`pU6At5IcWzuvmvQ0e z+}yr69^6-@QymWJcjn6ZcchnCPqv&q++PY@VJiE}$3Ns5nu>h@wBOIAC3h1puLt-$ zUK8mFPWQ?65)q8zd|oQpc?{IY6irU`4wnNH>m6oKoKWweUgGoZ#Cpd+Xn!{K4u(^` zV;}jz_f6bRf$=HR!IQ}__;nPb9kt2mP=`pRoBW9A?quB|PZs`pgWGpW7w6DF0Hu zLehz$!q@!VayZ0xg@bHo7-hRFYSk6I$E=_G4r#`Ge^tbqh$c83^vJ^!c0HtVvt|=L z*XU{V%hNTxZ=?Ew$tT|r@$+qd9uai7XdCGIsM4|0u~;i|{!8@+yC)FFjw)Qav{&KE zTlW#(=I@zviKpwS^Qp!}I7fo^nX6UeD|NoyS!GOm-r;P9v9AO8^qhl);lLLdhZUMv z)BR$UpY50O5LT5UpB=9y8jR+b=*PM%AmxcXf~Zey-g14`^oK95A^@%z98$HdPCt0w$GBDpYZqd>Yvs0q?_+AhWe*0Z0olAM=kuA z`c=E%GGchh`&W$b+M*rcEQaqJ{fYd-Xsu?!N@`6-S_pLCzi&S4L%_@?Fyy~HOR zBp(c{AYFhrTv%Xwlij-+SZMcQMqA}ZW$JSke&ouc`c98zWy!*9N3zoNDXTYGNk;X0 zk|or~^8D9a=N}@Sq`lBrhf7U;Kr7;(O#6S0^bz{)$mv0i?{WTl=DN6#soT%9E7)T3`39Qz$8U(>(Pt&VfnNZFZqu zvNa>&>1VT|?o{UenBE-53sK1KVR@||+d?IW0S6^o0b_YnSS@@X9Xl7`)G(0o6G^NgRbOXrPr9aqS#K7#aO>#=l8_ z89hN~pcY11pU;nOSAt$HtPRC&L+x736 zms>t52XH$p4C6cy!g2In_UD+j<6_i4emhQmPufw{|B|*2l#E1EU*U*i{G&E0envUxs+%34&ta^%k}p>+zt-7 zKNdL1zg4{oqJPqRkafm^&kuml?H)enHS|x?&GsVy{Tcb0blZ5Ac==!8mMUGc{D*UU zNq3R)ds)wX0E-j;*LcpK2=}$+KQ8>6T`!3=ca{Kj@Qxv)wqpB> zet#p3XmL@vjeeHzFdn>Jz@z^Uq}~S!2f2Je@J{Jz`2qK7$jze$C+)+ybUL^_7jT~h zT;1S=Y`X<6*gJ$s|2Wc*Sh^_SI=+l9t_MJ#^$7^zm&}xY%9PiSdJV5E+=3k3QE6uw zHM$!;Ku;-J%iSaGZr4kXYJU%Oou>$9>jj>`=cV6~a&10^odYh`Q)?oRB`9F&JRgRO zn(p6E8n3+1zG*x>^73N}%dv6e`a`Hcv+?BkyB#QwzNzh&{Ym<5Ul-4|G0}k^7+I*PdXzAI`=9sl*qiap3y;0(=R6z`73G9aro+f_Ml`9T%PIE*tH%28vT26a5|5_#(H z!)%slAXQycl)q6l(6LYbaFpfA{;|@<_p5;4et~fNxvzhR)^GwF?%t?v99mP-a)!G% zXgI7{s$th}!WwXq^n>X&Va+NFcPe~!O}~W|y>y=u^@EPh`OwET`!kOIS|FrcB)Ml_okH=CSKtto|{V6Q|^SDxPVL6o1W_L z5nQg>bbZ=#ekbODaeH?mo~?l3Ya2=kr|a`~u$c6oE_%P^d`@ikqsxuYB)#Q)PHg*R zF$)Tpk$>>F<$TUGvmcqAJ&tUj>>`#MHqrNcsB(Tx>5O?o`@`x_u46xhO+~E`>*quA z59@r)C+2G9{`2)`VcJrz(+^p`4ThhUxce$9H`6%Vdb&pey95B9=cEE+V z(|0{8^fTNO!)l$uHzq$pZvnj~x6`r$*Lx;hA78E2$U4sGTw;Hw^2YSbIQlIe7vbo` z^lkqwj=rY_pm#tIbG2ad4eKuShtIR=_k`B>YdNvL0wQ#<`{rRk=dFPakFp-)pRk{J z4Q$~1R_I4K=i9KK{4vn~Ma}2;T;hn~aM0$HX`D~OJw@ifNxi6N;f4Cc=~bArI1NKi z#u4{D!a?HQbH#wBheI6S$#v%6MfvFShQHI5+U0g?w$#4k=Ql8)&-jAK&%2op`uE9C zw`b0u!}ygV3^ab1*+0+Z{ORim*a3hDQMNxU=Y-<-iZH%gXd3HN{+ z!Us2@Au>-b{G`IC`u*EiX?%F2@yF%d-ziMtcOrei$G^u3d|Tw`Cc*_jRUB^L-b~F_Zn1pK?{;(Fr%&dw z-@M+0-68pQYd*2#)3@>C@6Lr$mY3(}WZ5dhujY6JAK$%8+V$Wu!{ey>s5ZL}@7W8k z!+56P%aVG`*$qCjjijZEjsVJo|~?pY~7UF9qW=&9C{scD4mBk zG9Gwd^=HJBG5WXGoQLfEX{MY#C})(=&@ aMqsD3HnQYt?!QCsdVb;qE@YZCq<vt^=5U$tsYg=cf z-?tpC5wH3l_5FJ^{{9Z=_ukJpzYBo($suV9)6duOjee6V-POEb(|O$VzsmIUgcgFI z{LA)pI5D6B>i+>DdeXEZ0b|SZ|vOysbA-pG~>QP|M!Cc*^fv3h;PzNcI{YqvZSi{*lZV z)ZcyG`yx~;{48v=Uwof8hDyY$`Ta@a@81cjmWl$6@fEQ@{oHJKr-g|}y8gBE80q@B zUL>6L*U&-x&g6PqU)#Bo;q@C7Zs;KW;q{!4h7J&GFUsqo2lvn-txW^x_dU$yrrlIx5_f;alZa2#OKi- zu#fzQzs*@r_U&I}>wg)KD1S42fmt+tuJ*UTe~5Fkz^D*Uux}rqrR)Fdi-3*n+fSdV z{h74~9hP{k_jR4WS-~QIZ^7T`O4solaO*g`jwk-m7tqd$*732P`k?*>e0}|%kesPr z=ltpSoYM1Y*8icOd@!)#pE%y!ZqrXWGq8c@*3$K~^|!C92Ua|;`I_r#hC}Zm7DJqzxH~u}_XZyWz#IT$L(D-E8ovdd;KZ;51jm>?FJsJ=Em9Kq2uD7W1^4&Sh z&wkJ9o6uJ`Xhz6chASG z_wRS0-V?-cl=bKL(R-G&WBi@T(8YEn%M2fDZ+Z{U_8JMuEq?_lqsCK`mG~6^)A=i_Hy5Uju|3NUsGjBaz-0AN>XT+K%hG?)>P1Pw&?O-^=7F7pE8a3-t2+wQ%@; zEjNy8nsK6i(1d6UJ$5QwIJ}#D`QQ%qolc;lbcws#MD_Z)A>nU@MZf3Q?MuL~lG@5= zuhxF^^QUq@MDfq_8|V*8X6Xce^Y`mPm)Crj;X%^X?a%&QJRKJ;>+yA(Z=j3c;Xr!p zdVGVWe@lJT3y1O2x*p#`xL(T9N=L84g~O{fKDFQ3c|n(#px*~S*Bl2G(c@4>Vfy0e zqo_yzNmcG@-X5`YXn$3v_XsbI)4wyiHD6e~Tl1#pWTih_*4-<45G{<6e~0+oQ`ESL zRr)(>$rXaAxNv<@{rtMGd^ri@@45K-N#JiP74p@d>AKt7oeU8VUnjYK`wqoQ@H=91 zL)c_c;{3eiWb5p(n0irq57p&Hy52UqA)Qf_aE633VGgWQ)>Iu-1@So!sW*kOp5-@j~IXC6{Qw5>C5FQ)N; zz7QM~+epqM{e4Qz9r?<|$Sn2EQsC1n<&DNQ@`U=x7%&jvo(DB{2&J_CT zI`eX(nq0?mE#GY-KZ#BThxw~d{KWPH(|yBpG&x!+jnMQ5xNkV#KB3M7hY0WT0{r%~ zqe3sq-)hIU`M}QgrRRa_%ny9DysZqoJVJSkb_;zEn_REw%K?^>J|qReALY5Jo|5g) zN;Fam9WT{%@b8N^HG2r_>hmN8^~f-O67n5*yL_EUzjl4p@9U!+)Z^=Jx9_Lx?W?u= zmirOtxEeN7K4nf=V} z2cW+BQbbnHMr{;^-@cA7UTKK8Ch) zpk({ALuYA&WPj1(KdJCO?}n{p&@lE0)0f-+EH;D0`TlHUy9V_hY+Zh4<>m> z*?!252h{WOIWn#UkCt(j>RFN?&fn{C{%!Kx^Aj-d^OJ-a-22uAJzP* ze_d|f^N0UJyz+L{lM-K;M(d5Af5&;^KEbnju8;yh*-Jig{lN8SA76eC8G8Gm+dR3J`$i}a&e!RHYndl)J`D$#YIy~F zj~4ycVoyu$Kwl<39_VEKg?^TA@1{2GS91AX9L6K$Wl|7=hzr*i)kpj_@(Xq$LlN<{ z^4&W%y>P}3+GVe{`CWjT(pb)`**-`0tTN#{wyN*@rkE%6T>~rJkzZ}~ zcU`OLXumrY;d+qY+rT`h?;04K+avwEw{9P-_q@N68F^drDK`mbE?X-ibT%l&-CXQsvPTthq^-sAn;;OV|YP`=yj zmR|lsmOH;hztAx|!cVswf@yj3pRc+Zsh;S$7>u7X#3N5C+hbz zY<5lPyDjsK(P!WqN)O0`l9cEBR@F5{2|>>i#uNv9)`}lnDPgpz11i(FA)S72w?sMKl=H6@tTS@n=oxy88lNmG_J6@2@AjIG_7DdYM;F0l?2i zEs=idlQ71`ZjmD^EiC)!3fFk278f;!>5sSmf&B4vlEKb1X4gSqKG1A`9`7c%e~h45UkxYy&(g3ER2=xNW&Q>;igS4zM|(+ElW$4)Dx>qA>L<(k7#_3nZu&tg zH%#vMy)xHVz{iVFkDb$+n2(8_%U6eQ>Y0W|dhe`8ymIYudExe$RDKyB#?kef5A@id z(?@UmXQ7kKbH{S;O8l}&ZoePVxJSu0F5H_04(0C2mAg~Q4O^Kn>@}a_D(YE|UX%V3 zPqIq?Q40;;PFn{J^xAg-eLM}bUk7@h(DwLz?e>o_Og(8}Huop}S=hP+Xzo zXE~4F-LK&=O8u=YZxF$`d`ACa2P%%5>nS0Tg#Up2VEKH$NzUn))Yf$&_}%4j7;BEp z3tlgDIm~|b`8Ln*1ErFd2RzWCs?o79<^628#6v_>xAHt+Vajh4udV2L8K*{9*P~sp zay{7Fw^pc&C%cZ+oT&AF^rLpC=clc`#zG}ZGlfo z%FW9y$h}|t81^H{*Od*aSVJn}Eq{CrQLdX(cld#`T| zIZK}*gzLyhISc+l{YeXbv|ynN*B|4x>!YXHu36eH)Y{svtq70Pt}_Kb3>kk3 z8o*Ib)S^mT%U7w;_GX^vLpCS z@V-uB?T&SqTHu!LR{WE0vuAB8o|59_T^zgL_iE0!VT;Yzw(d^fwXk&)s68n8^njiIPlxKa)hi}-=h6e==d>>4`v5Rx@T&97~R2sTQ-B?u|bAcKdJSF zgT&wU?fiamYG1f(2iv=A3;hQMiSL72t*&pL$l39)1^#z49piLRegf~`lSXITc|Y>q z(+>Ar0v8@6UBY4ZYtK%eH}G;nKlE$Ty@7Z+9j#u}_mMvZzwXiWP%^uQ&2O+TwCoeC z6$Mos@Rg!v{7M1hMEedP(1j9Ya{I<#{T29+^k~{Q`qFY(GRtYVZ!A?f^v6a41V8@{#zmd^MGtLD|D2?6 z7ucfwb9-;TADy4WA-1Pt?`=As)!svryS@J_QYx>6?`gDhDrB@bsQ;)^53T8}V#iKT$6Bl!}_M zt-RZ;ykYvF>)&3Fan5}C`LDUIIZrbL+pnp16)jBp;O{6nT?Ha<36Me?MWOb&m6&;~z(MuFB+3zJ1{~ zj{nfBWEjUz+$v8CQ-Lk}D=WX`U0KT0UdmU<%W>uDg!bc{Jhk@)+wI4cr_J-YCSTve ze(>`st|!IOD-~bpi7F>g<9-?V75vn7qtN@P&Ci_+wVY&^%|l~**k0RT%JZkJXLbA} zCPz`;9vRel*KS-sA7TfDI>%8M<$Rc~dzMiTSt;%46HMG55bEn#kJ%CYU7GRi0y+*U z7KD#>YrgFL$&s9yW?Es549dtcQ;hv*m!C&d(_VQr+Kz^SljM+X< zs<)6|5|TIa^?j3ndCng7ERq@~vTIeFfh?N-S+>R z>-4j=;-S61PX8HgSDt^vsLfAfoF~A)R|*S+Bc$Kta+cae)!JF;c&^qPs-PnJ@ubqX zr$oL%{~{&UufWNCF98v;68@--GwaVW%IS1nI%51|bj;cjAldxg^pAENbt#~Yu49A;J->Op zssC5(U3b*`63HjumC|-2jFMw{jYqtwdiYxPv+wle*P%L(-wkN$!Iy}S`Yw^XOC#HX zmEdLl0=^za_#TaJSzli)@L4$I%&z01_ha7cmw1%_A%V~9Ed{k2vOh7C#?c*$Kh|@P zp*+JQjBZgnqCAKKly|9|Px^`Un^|Jm*TrQ!uiL&Z?ohxug9lb1adfld zk<9DY^t`^{`c*oQr|SS?EPQz`uh5deMR;iP>{_yo? z*tG5x5|Z`D6u+>k-@^A>c$I|@Sh&x^`z^fG!bdI4^?9=XxP`e+Pu4%o@U^yH-^6(` zS$`+vw{W49{r>8H#p@BKuUH2(<=qDL)CSnHo{ZUU-#0776X`#8e#G8?fb40}e_~rt zUOr1JLVC+Nv)J?>R+qj90l1d+?~lCLs98tEOJcrbS{6-Cs zOB=<9T$}xuf^VI4f*b+8#r~0&!kX9mb>W3jXa1}l_xd`=k<0o1 z6}?UC4`XbX$YX{dB)#!n7r1cH`uFlf8qSwfiJ!0Sb-1?mWlgh5ds$zep4s{GKDq^TJ^6h=rsteTPia`r6R0omQfbG8F>CJumS4V&@xEW) z++Tlz%D;y3oA0APYy0bai$FI@%U`I$^j-f#$JuJnkM*lQ>y4)PhqyjV~NeUl*p|uQC31Ig9`z}c_LrGjnkeI?JI5Njgqd=qaVRL zi(@v=AGG=Z&@q)yD1TU%7F8=Rn7k@<{J0k2bV$12@I6F`p09kKEZffT80nL&el5cX zO)gD%|LX138_h24e2|zOx%u4&?uWIt%k=Ezu71Ve`5W!{l|jMpDDiPQmZ#f5iR-9* zxyb+J_c!HVq2pp}zm;e0mURrrySLNMZKV4KHXh`CJ!N~Rmye@#-DT_1=D8BS^Ov^k zs8+9b#Vy~H{eet>eo^GQpOeQtc0g(Z{_mB!wxwOt@7eb~9qzEe$vsofPopQ)_j4A> zGV+PM$FK32U;6}=q?`OA_cxi(=1ZiPBt47YmhaO3v+GmPMFc>ce_x;lKeQv$u9Jc< z+9NE>^~)!OFOPmv>FxYkknfbkZR^~uCz5=9SLpmnt*FrX^I9X~UnX#Ibgjm_d~>=1 zZ)mPz-*qT1D)*vRv*U{IaB|*O+gq5zbh($H;mVYz9?KNXUUNMlDQQ^he$Z2u$fkm~%@_4SmUlMShrWLr3Vn)43w|;#knorKXTbSd)<$>!vD}_!d z$>oB}3HdIH<>&g}zegeOuV{HK^+G-v9ekZ5_n6c6|AN%v^KbKgv=MZd#9RAj??F(` zY428lqVn;+2Id91$ExLL?d`uSV*>V|?06EpsLmJ7_X@Aqdb8!@y@Bz@>z`i+J&5Os z3LRe{er@j!JgofY@Aaqe+Sxn(azDhG5-Gc^-a1LI{uLGV^2_DG5vnLeE%x#o}7+W-YD@4B`rQ< z$749u*-zfy=04Kp)^6*^O#UCX-G9*1?jxt&ZuECvKTP>H9hX+#WbGbZ$9}eTCHfN$ z_I;DAy;XZ(|lt9p~J%J<#mAGeDGZW`e7`r%&Y^K)8&d-5Lzj_B)s zDV1kc!pS|XwBGLtT&SO(yh!=dWm7V(=;A*eX*t^mh)Lj!;)a(mxzD5 z`k=4l;^=FNhvOAS+0T>fp=NI#Gdo9qz0>xNiR}kCN*x4Cnhht5@XL5A~0Yr&o}key%B7-emgClX7O`ndmqB z$Hilbw%6Ag7_Wc#jplpR-e1G^4iNY(C|Bly0e3*cV#Dz)Zlbna*JdxT#22~%D z{XUHsJ0rvS^>2<3eE-?+`?=lF`PSv3>&4A`iJZ67edGz`Z5)lMT*&s@WcuAB+HcRE zemC-~B3C*mkSlwgzhH%j+@O39|Hb;8+!t4Q|4j6`&kOIR@-J=Q#cZF=U!gR}@ND(u z{-XH{>G!OlK@jT&P{_}bLhmU4scagU!#KF*o2c&z>QA=*ZSu`2jc@V|=LzQdf{3O6h;ZpUUm$u8?@&UroQWIG6Q=HS{3|TlODI z5+Tm%@AqE98X3Ui0N=9DSh9P%YlIOky=9-V)I3jy^l<4sczRyDWuI|5=fg_JD>OY^ z&2^c4&x`cFn(H=MM_AbQW4ljT*zGqqe_l)dDqQV+@TkK1z81z!zt}1KyiHT@zESH( z`*l6x=eLH6jFwpf%!icYxXRp?D*g^Fj4EwGK;F_aV-dkMz(+Y?4QYe2je2G~! z)xMKxg{Jn44tob9S0C<=9hQ96wPfVVRDLJX_pM;ZMmqFrJ)dE6COPxC(ji&@2F4n_vcmDsXHRNZEA7MF}r zF_R^1XL1MW5J#Wabikv9ar7C&@1>NK_fb)7G%%azQ^Hb_2znK7zgOZ#nI7}p6K5=;&;ij z^<9i{KdRt*A1;sgdc}K{mgC&weLqY}8L!?#G?RMM zO4FNWJXhx}*OPG$@vxMK;#6<4avL>?5Qo~(6LBpygnp}M75yHutHC*c z75v=?DF;P0^RXRa75#zE?{Xa4yV$|rmmBCbyNQlH^7-Aa*%)C%T;D{w zo!3gc?0$3dz){81=N(y3k}mfCfa@XQ5a;Xinm^P6%Qt;Weg9s-Ob&y{zi01yGNQpZ z=4zFx7b_p`+{>6oKmD0x!}LC4S@!DSz<26P5aXv}_u5<@qWlcY)XLSe!q2=yy=eX# z`i`gT$KmLHYp1pMz?~Xy({D`=^Y#z)m9)ToyDa?2-n+D4(8+G0{IgBmvOa(*`%G^kM;Lxk~b4Ri3uG?KcC|x@=w#gLwYyu zJLGG(?>OJZ(HmKg>seEGC`JRbAGdhZv!?F0c(csw($dD zDbD2{#?9v&(r<_NYQ6qGK(ib&aJHO}<@7Gx=SO*$NVzBvE9a42AJy>(y`?7c+5C_L zu9sx-`1rVZP``C~gYvs&Lk4^Tgk}Fy>zT~X+@$*z+RiLKQ-IH?6ebS(VZJ@{v^^cn zcUu##BtNs|XuHNNUFh*qp%d_({xI||@5*-v1&G zcZWbHl;iJa%D$JDhkB?V9@DVfcOYLM$-V3H=)X$4)}VCpX~t4JTOO_fIC}@-f9!(o zcEOLx)cikoLGlr}8SB2MvJ0+ymGZOl=l|FRZT^N5A

-bkyKzHdDJ`C;wv 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

90}~Kxm;0+eM&0GD$58R;fZMm*1(u6~yt(hm42 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 I_j!xJF<*9hy@!eKTqVFr^ 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 literal 0 HcmV?d00001 diff --git a/tests/lpPool.ts b/tests/lpPool.ts new file mode 100644 index 0000000000..bc8a141023 --- /dev/null +++ b/tests/lpPool.ts @@ -0,0 +1,1680 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, +} from '@solana/web3.js'; +import { + createAssociatedTokenAccountInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddress, + getAssociatedTokenAddressSync, + getMint, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; + +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + encodeName, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + AmmConstituentMapping, + LPPoolAccount, + getConstituentVaultPublicKey, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_RATE_PRECISION, + getAmmCachePublicKey, + AmmCache, + ZERO, + getConstituentPublicKey, + ConstituentAccount, + PositionDirection, + getPythLazerOraclePublicKey, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + BASE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + getTokenAmount, + TWO, + ConstituentLpOperation, +} from '../sdk/src'; + +import { + createWSolTokenAccountForUser, + initializeQuoteSpotMarket, + initializeSolSpotMarket, + mockAtaTokenAccountForMint, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccountWithAuthority, + overWriteMintAccount, + overWritePerpMarket, + overWriteSpotMarket, + setFeedPriceNoProgram, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_LAZER_HEX_STRING_SOL, PYTH_STORAGE_DATA } from './pythLazerData'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let userLpTokenAccount: PublicKey; + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + let spotMarketOracle2: PublicKey; + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + let solUsd: PublicKey; + let solUsdLazer: PublicKey; + + const lpPoolName = 'test pool 1'; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey( + program.programId, + encodeName(lpPoolName) + ); + + let whitelistMint: PublicKey; + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200); + spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(1_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + + const periodicity = new BN(0); + + solUsdLazer = getPythLazerOraclePublicKey(program.programId, 6); + await adminClient.initializePythLazerOracle(6); + + await adminClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + await adminClient.initializePerpMarket( + 2, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(2, 1); + + await adminClient.updatePerpAuctionDuration(new BN(0)); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + 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(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + await initializeSolSpotMarket(adminClient, spotMarketOracle2); + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + await adminClient.initializeLpPool( + lpPoolName, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() + ); + + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + // Give the vamm some inventory + await adminClient.openPosition(PositionDirection.LONG, BASE_PRECISION, 0); + await adminClient.openPosition(PositionDirection.SHORT, BASE_PRECISION, 1); + assert( + adminClient + .getUser() + .getActivePerpPositions() + .filter((x) => !x.baseAssetAmount.eq(ZERO)).length == 2 + ); + + console.log('create whitelist mint'); + const whitelistKeypair = Keypair.generate(); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: bankrunContextWrapper.provider.wallet.publicKey, + newAccountPubkey: whitelistKeypair.publicKey, + space: MINT_SIZE, + lamports: 10_000_000_000, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + whitelistKeypair.publicKey, + 0, + bankrunContextWrapper.provider.wallet.publicKey, + bankrunContextWrapper.provider.wallet.publicKey, + TOKEN_PROGRAM_ID + ) + ); + + await bankrunContextWrapper.sendTransaction(transaction, [ + whitelistKeypair, + ]); + + const whitelistMintInfo = + await bankrunContextWrapper.connection.getAccountInfo( + whitelistKeypair.publicKey + ); + console.log('whitelistMintInfo', whitelistMintInfo); + + whitelistMint = whitelistKeypair.publicKey; + }); + + after(async () => { + await adminClient.unsubscribe(); + }); + + it('can create a new LP Pool', async () => { + // check LpPool created + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminClient.wallet.publicKey + ); + + // Check amm constituent map exists + const ammConstituentMapPublicKey = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammConstituentMap = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapPublicKey + )) as AmmConstituentMapping; + expect(ammConstituentMap).to.not.be.null; + assert(ammConstituentMap.weights.length == 0); + + // check constituent target weights exists + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 0); + + // check mint created correctly + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(0); + expect(mintInfo.mintAuthority?.toBase58()).to.equal(lpPoolKey.toBase58()); + }); + + it('can add constituents to LP Pool', async () => { + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [], + }); + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.constituents == 1); + + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 1); + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentTokenVault = + await bankrunContextWrapper.connection.getAccountInfo( + constituentVaultPublicKey + ); + expect(constituentTokenVault).to.not.be.null; + + // Add second constituent representing SOL + await adminClient.initializeConstituent(lpPool.name, { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + derivativeWeight: ZERO, + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO], + }); + }); + + it('can add amm mapping datum', async () => { + // Firt constituent is USDC, so add no mapping. We will add a second mapping though + // for the second constituent which is SOL + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 1, + constituentIndex: 1, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 1); + }); + + it('can update and remove amm constituent mapping entries', async () => { + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + let ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 2); + + // Update + await adminClient.updateAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION.muln(2), + }, + ]); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert( + ammMapping.weights + .find((x) => x.perpMarketIndex == 2) + .weight.eq(PERCENTAGE_PRECISION.muln(2)) + ); + + // Remove + await adminClient.removeAmmConstituentMappingData( + encodeName(lpPoolName), + 2, + 0 + ); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.find((x) => x.perpMarketIndex == 2) == undefined); + assert(ammMapping.weights.length === 1); + }); + + it('can crank amm info into the cache', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + expect(ammCache).to.not.be.null; + assert(ammCache.cache.length == 3); + assert(ammCache.cache[0].oracle.equals(solUsd)); + assert(ammCache.cache[0].oraclePrice.eq(new BN(200000000))); + }); + + it('can update constituent properties and correlations', async () => { + const constituentPublicKey = getConstituentPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + constituentPublicKey + )) as ConstituentAccount; + + await adminClient.updateConstituentParams( + encodeName(lpPoolName), + constituentPublicKey, + { + costToTradeBps: 10, + } + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const targets = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBase + )) as ConstituentTargetBaseAccount; + expect(targets).to.not.be.null; + assert(targets.targets[constituent.constituentIndex].costToTradeBps == 10); + + await adminClient.updateConstituentCorrelationData( + encodeName(lpPoolName), + 0, + 1, + PERCENTAGE_PRECISION.muln(87).divn(100) + ); + + await adminClient.updateConstituentCorrelationData( + encodeName(lpPoolName), + 0, + 1, + PERCENTAGE_PRECISION + ); + }); + + it('fails adding datum with bad params', async () => { + // Bad perp market index + try { + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 3, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + expect(e.message).to.contain('0x18ae'); + } + + // Bad constituent index + try { + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 0, + constituentIndex: 5, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + expect(e.message).to.contain('0x18ae'); + } + }); + + it('fails to add liquidity if aum not updated atomically', async () => { + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.lpPoolAddLiquidity({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + }); + expect.fail('should have failed'); + } catch (e) { + assert(e.message.includes('0x18b7')); + } + }); + + it('fails to add liquidity if a paused operation', async () => { + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + ConstituentLpOperation.Deposit + ); + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + 0 + ); + }); + + it('can update pool aum', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.constituents == 2); + + const createAtaIx = + adminClient.createAssociatedTokenAccountIdempotentInstruction( + await getAssociatedTokenAddress( + lpPool.mint, + adminClient.wallet.publicKey, + true + ), + adminClient.wallet.publicKey, + adminClient.wallet.publicKey, + lpPool.mint + ); + + await adminClient.sendTransaction(new Transaction().add(createAtaIx), []); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.lastAum.eq(new BN(1000).mul(QUOTE_PRECISION))); + + // Should fail if we dont pass in the second constituent + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + try { + await adminClient.updateLpPoolAum(lpPool, [0]); + expect.fail('should have failed'); + } catch (e) { + assert(e.message.includes('0x18b3')); + } + }); + + it('can update constituent target weights', async () => { + await adminClient.postPythLazerOracleUpdate([6], PYTH_LAZER_HEX_STRING_SOL); + await adminClient.updatePerpMarketOracle( + 0, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 1, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 2, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updateAmmCache([0, 1, 2]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + tx.add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ] + ) + ); + await adminClient.sendTransaction(tx); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + assert( + constituentTargetBase.targets.filter((x) => x.targetBase.eq(ZERO)) + .length !== constituentTargetBase.targets.length + ); + }); + + it('can add constituent to LP Pool thats a derivative and behave correctly', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.name, { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + oracleStalenessThreshold: new BN(400), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(2), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION.muln(87).divn(100)], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ] + ) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[1].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[2].targetBase.toNumber(), + 10 + ); + + // Move the oracle price to be double, so it should have half of the target base + const derivativeBalanceBefore = constituentTargetBase.targets[2].targetBase; + const derivative = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + await setFeedPriceNoProgram(bankrunContextWrapper, 400, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ] + ) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + const derivativeBalanceAfter = constituentTargetBase.targets[2].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + expect(derivativeBalanceAfter.toNumber()).to.be.approximately( + derivativeBalanceBefore.toNumber() / 2, + 20 + ); + + // Move the oracle price to be half, so its target base should go to zero + const parentBalanceBefore = constituentTargetBase.targets[1].targetBase; + await setFeedPriceNoProgram(bankrunContextWrapper, 100, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx3 = new Transaction(); + tx3 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ] + ) + ); + await adminClient.sendTransaction(tx3); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + const parentBalanceAfter = constituentTargetBase.targets[1].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect(parentBalanceAfter.toNumber()).to.be.approximately( + parentBalanceBefore.toNumber() * 2, + 10 + ); + await setFeedPriceNoProgram(bankrunContextWrapper, 200, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + }); + + it('can settle pnl from perp markets into the usdc account', async () => { + await adminClient.updateFeatureBitFlagsSettleLpPool(true); + + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + // Exclude 25% of exchange fees, put 100 dollars there to make sure that the + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(0, 100, 25); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(1, 100, 0); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(2, 100, 0); + + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.totalExchangeFee = perpMarket.amm.totalExchangeFee.add( + QUOTE_PRECISION.muln(100) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + await adminClient.depositIntoPerpMarketFeePool( + 0, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + await adminClient.depositIntoPerpMarketFeePool( + 1, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterDeposit = lpPool.lastAum; + + // Make sure the amount recorded goes into the cache and that the quote amount owed is adjusted + // for new influx in fees + const ammCacheBeforeAdjust = ammCache; + // Test pausing tracking for market 0 + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 1); + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + assert(ammCache.cache[0].lastFeePoolTokenAmount.eq(ZERO)); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool + ) + ); + assert(ammCache.cache[1].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert( + ammCache.cache[1].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[1].quoteOwedFromLpPool.sub( + new BN(100).mul(QUOTE_PRECISION) + ) + ) + ); + + // Market 0 on the amm cache will update now that tracking is permissioned again + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 0); + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + assert(ammCache.cache[0].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool.sub( + new BN(75).mul(QUOTE_PRECISION) + ) + ) + ); + + const usdcBefore = constituent.vaultTokenBalance; + // Update Amm Cache to update the aum + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterUpdateCacheBeforeSettle = lpPool.lastAum; + assert( + lpAumAfterUpdateCacheBeforeSettle.eq( + lpAumAfterDeposit.add(new BN(175).mul(QUOTE_PRECISION)) + ) + ); + + // Calculate the expected transfer amount which is the increase in fee pool - amount owed, + // but we have to consider the fee pool limitations + const pnlPoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + const pnlPoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + // Expected transfers per pool are capital constrained by the actual balances + const expectedTransfer0 = BN.min( + ammCache.cache[0].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance0.add(feePoolBalance0).sub(QUOTE_PRECISION.muln(25)) + ); + const expectedTransfer1 = BN.min( + ammCache.cache[1].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance1.add(feePoolBalance1) + ); + const expectedTransferAmount = expectedTransfer0.add(expectedTransfer1); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterSettle = lpPool.lastAum; + assert(lpAumAfterSettle.eq(lpAumAfterUpdateCacheBeforeSettle)); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const usdcAfter = constituent.vaultTokenBalance; + const feePoolBalanceAfter = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + console.log('usdcBefore', usdcBefore.toString()); + console.log('usdcAfter', usdcAfter.toString()); + + // Verify the expected usdc transfer amount + assert(usdcAfter.sub(usdcBefore).eq(expectedTransferAmount)); + console.log('feePoolBalanceBefore', feePoolBalance0.toString()); + console.log('feePoolBalanceAfter', feePoolBalanceAfter.toString()); + // Fee pool can cover it all in first perp market + expect( + feePoolBalance0.sub(feePoolBalanceAfter).toNumber() + ).to.be.approximately(expectedTransfer0.toNumber(), 1); + + // Constituent sync worked successfully + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + assert( + new BN(constituentVault.amount.toString()).eq( + constituent.vaultTokenBalance + ) + ); + }); + + it('will settle gracefully when trying to settle pnl from constituents to perp markets if not enough usdc in the constituent vault', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + /// First remove some liquidity so DLP doesnt have enought to transfer + const lpTokenBalance = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const tx = new Transaction(); + tx.add( + ...(await adminClient.getAllSettlePerpToLpPoolIxs(lpPool.name, [0, 1, 2])) + ); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + ...(await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(lpTokenBalance.amount.toString()), + minAmountOut: new BN(1000).mul(QUOTE_PRECISION), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(tx); + + let constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + const expectedTransferAmount = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const constituentUSDCBalanceBefore = constituentVault.amount; + + // Temporarily overwrite perp market to have taken a loss on the fee pool + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const spotMarket = adminClient.getSpotMarketAccount(0); + const perpMarket = adminClient.getPerpMarketAccount(0); + spotMarket.depositBalance = spotMarket.depositBalance.sub( + perpMarket.amm.feePool.scaledBalance.add( + spotMarket.cumulativeDepositInterest.muln(10 ** 3) + ) + ); + await overWriteSpotMarket( + adminClient, + bankrunContextWrapper, + spotMarket.pubkey, + spotMarket + ); + perpMarket.amm.feePool.scaledBalance = ZERO; + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + /// Now finally try and settle Perp to LP Pool + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + await adminClient.sendTransaction(settleTx); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + constituentVault = await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + // Should have written fee pool amount owed to the amm cache and new constituent usdc balane should be 0 + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + // No more usdc left in the constituent vault + assert(constituent.vaultTokenBalance.eq(ZERO)); + assert(new BN(constituentVault.amount.toString()).eq(ZERO)); + + // Should have recorded the amount left over to the amm cache and increased the amount in the fee pool + assert( + ammCache.cache[0].lastFeePoolTokenAmount.eq( + new BN(constituentUSDCBalanceBefore.toString()) + ) + ); + expect( + ammCache.cache[0].quoteOwedFromLpPool.toNumber() + ).to.be.approximately( + expectedTransferAmount + .sub(new BN(constituentUSDCBalanceBefore.toString())) + .toNumber(), + 1 + ); + assert( + adminClient + .getPerpMarketAccount(0) + .amm.feePool.scaledBalance.eq( + new BN(constituentUSDCBalanceBefore.toString()).mul( + SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION) + ) + ) + ); + + // Update the LP pool AUM + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.lastAum.eq(ZERO)); + }); + + it('perp market will not transfer with the constituent vault if it is owed from dlp', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market half of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .div(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + expect( + ammCache.cache[0].quoteOwedFromLpPool.toNumber() + ).to.be.approximately(owedAmount.divn(2).toNumber(), 1); + assert(constituent.vaultTokenBalance.eq(ZERO)); + assert(lpPool.lastAum.eq(ZERO)); + + // Deposit here to DLP to make sure aum calc work with perp market debt + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + let aum = new BN(0); + for (let i = 0; i <= 2; i++) { + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + aum = aum.add( + constituent.vaultTokenBalance + .mul(constituent.lastOraclePrice) + .div(QUOTE_PRECISION) + ); + } + + // Overwrite the amm cache with amount owed + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + for (let i = 0; i <= ammCache.cache.length - 1; i++) { + aum = aum.sub(ammCache.cache[i].quoteOwedFromLpPool); + } + assert(lpPool.lastAum.eq(aum)); + }); + + it('perp market will transfer with the constituent vault if it should send more than its owed', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const aumBefore = lpPool.lastAum; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + const balanceBefore = constituent.vaultTokenBalance; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market half of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .mul(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(ammCache.cache[0].quoteOwedFromLpPool.eq(ZERO)); + assert(constituent.vaultTokenBalance.eq(balanceBefore.add(owedAmount))); + assert(lpPool.lastAum.eq(aumBefore.add(owedAmount.muln(2)))); + }); + + it('can work with multiple derivatives on the same parent', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.name, { + spotMarketIndex: 3, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ + ZERO, + PERCENTAGE_PRECISION.muln(87).divn(100), + PERCENTAGE_PRECISION, + ], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateConstituentParams( + lpPool.name, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + } + ); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ] + ) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[2].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[3].targetBase.toNumber(), + 10 + ); + expect( + constituentTargetBase.targets[3].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[1].targetBase.toNumber() / 2, + 10 + ); + + // Set the derivative weights to 0 + await adminClient.updateConstituentParams( + lpPool.name, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: ZERO, + } + ); + + await adminClient.updateConstituentParams( + lpPool.name, + getConstituentPublicKey(program.programId, lpPoolKey, 3), + { + derivativeWeight: ZERO, + } + ); + + const parentTargetBaseBefore = constituentTargetBase.targets[1].targetBase; + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ] + ) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + const parentTargetBaseAfter = constituentTargetBase.targets[1].targetBase; + + expect(parentTargetBaseAfter.toNumber()).to.be.approximately( + parentTargetBaseBefore.toNumber() * 2, + 10 + ); + }); + + it('cant withdraw more than constituent limit', async () => { + await adminClient.updateConstituentParams( + encodeName(lpPoolName), + getConstituentPublicKey(program.programId, lpPoolKey, 0), + { + maxBorrowTokenAmount: new BN(10).muln(10 ** 6), + } + ); + + try { + await adminClient.withdrawFromProgramVault( + encodeName(lpPoolName), + 0, + new BN(100).mul(QUOTE_PRECISION) + ); + } catch (e) { + console.log(e); + assert(e.toString().includes('0x18b9')); // invariant failed + } + }); + + it('cant disable lp pool settling', async () => { + await adminClient.updateFeatureBitFlagsSettleLpPool(false); + + try { + await adminClient.settlePerpToLpPool(encodeName(lpPoolName), [0, 1, 2]); + assert(false, 'Should have thrown'); + } catch (e) { + assert(e.message.includes('0x18bd')); + } + + await adminClient.updateFeatureBitFlagsSettleLpPool(true); + }); + + it('can do spot vault withdraws when there are borrows', async () => { + // First deposit into wsol account from subaccount 1 + await adminClient.initializeUserAccount(1); + const pubkey = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + new BN(7_000).mul(new BN(10 ** 9)) + ); + await adminClient.deposit(new BN(1000).mul(new BN(10 ** 9)), 2, pubkey, 1); + const lpPool = await adminClient.getLpPoolAccount(encodeName(lpPoolName)); + + // Deposit into LP pool some balance + const ixs = []; + ixs.push(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + ixs.push( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 2, + minMintAmount: new BN(1), + lpPool, + inAmount: new BN(100).mul(new BN(10 ** 9)), + })) + ); + await adminClient.sendTransaction(new Transaction().add(...ixs)); + await adminClient.depositToProgramVault( + lpPool.name, + 2, + new BN(100).mul(new BN(10 ** 9)) + ); + + const spotMarket = adminClient.getSpotMarketAccount(2); + spotMarket.depositBalance = new BN(1_186_650_830_132); + spotMarket.borrowBalance = new BN(320_916_317_572); + spotMarket.cumulativeBorrowInterest = new BN(697_794_836_247_770); + spotMarket.cumulativeDepositInterest = new BN(188_718_954_233_794); + await overWriteSpotMarket( + adminClient, + bankrunContextWrapper, + spotMarket.pubkey, + spotMarket + ); + + // const curClock = + // await bankrunContextWrapper.provider.context.banksClient.getClock(); + // bankrunContextWrapper.provider.context.setClock( + // new Clock( + // curClock.slot, + // curClock.epochStartTimestamp, + // curClock.epoch, + // curClock.leaderScheduleEpoch, + // curClock.unixTimestamp + BigInt(60 * 60 * 24 * 365 * 10) + // ) + // ); + + await adminClient.withdrawFromProgramVault( + encodeName(lpPoolName), + 2, + new BN(500).mul(new BN(10 ** 9)) + ); + }); + + it('whitelist mint', async () => { + await adminClient.updateLpPoolParams(encodeName(lpPoolName), { + whitelistMint: whitelistMint, + }); + + const lpPool = await adminClient.getLpPoolAccount(encodeName(lpPoolName)); + assert(lpPool.whitelistMint.equals(whitelistMint)); + + console.log('lpPool.whitelistMint', lpPool.whitelistMint.toString()); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + try { + await adminClient.sendTransaction(tx); + assert(false, 'Should have thrown'); + } catch (e) { + assert(e.toString().includes('0x1789')); // invalid whitelist token + } + + const whitelistMintAta = getAssociatedTokenAddressSync( + whitelistMint, + adminClient.wallet.publicKey + ); + const ix = createAssociatedTokenAccountInstruction( + bankrunContextWrapper.context.payer.publicKey, + whitelistMintAta, + adminClient.wallet.publicKey, + whitelistMint + ); + const mintToIx = createMintToInstruction( + whitelistMint, + whitelistMintAta, + bankrunContextWrapper.provider.wallet.publicKey, + 1 + ); + await bankrunContextWrapper.sendTransaction( + new Transaction().add(ix, mintToIx) + ); + + const txAfter = new Transaction(); + txAfter.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + txAfter.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + + // successfully call add liquidity + await adminClient.sendTransaction(txAfter); + }); +}); diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts new file mode 100644 index 0000000000..831335fa36 --- /dev/null +++ b/tests/lpPoolCUs.ts @@ -0,0 +1,663 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + AddressLookupTableProgram, + ComputeBudgetProgram, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js'; +import { + createInitializeMint2Instruction, + getMint, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; + +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + encodeName, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + AmmConstituentMapping, + LPPoolAccount, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_RATE_PRECISION, + getAmmCachePublicKey, + AmmCache, + ZERO, + getConstituentPublicKey, + ConstituentAccount, + PositionDirection, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + BASE_PRECISION, +} from '../sdk/src'; + +import { + initializeQuoteSpotMarket, + mockAtaTokenAccountForMint, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccountWithAuthority, + overwriteConstituentAccount, + sleep, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_STORAGE_DATA } from './pythLazerData'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +const NUMBER_OF_CONSTITUENTS = 10; +const NUMBER_OF_PERP_MARKETS = 60; +const NUMBER_OF_USERS = Math.ceil(NUMBER_OF_PERP_MARKETS / 8); + +const PERP_MARKET_INDEXES = Array.from( + { length: NUMBER_OF_PERP_MARKETS }, + (_, i) => i +); +const SPOT_MARKET_INDEXES = Array.from( + { length: NUMBER_OF_CONSTITUENTS + 2 }, + (_, i) => i +); +const CONSTITUENT_INDEXES = Array.from( + { length: NUMBER_OF_CONSTITUENTS }, + (_, i) => i +); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let userLpTokenAccount: PublicKey; + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + let spotMarketOracle2: PublicKey; + + let adminKeypair: Keypair; + + let lutAddress: PublicKey; + + const userClients: TestClient[] = []; + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + let solUsd: PublicKey; + + const lpPoolName = 'test pool 1'; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey( + program.programId, + encodeName(lpPoolName) + ); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + 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(); + const imfFactor = 0; + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200); + spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); + + const keypair = new Keypair(); + adminKeypair = keypair; + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 12); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(1_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + + await adminClient.initializePythLazerOracle(6); + + await adminClient.updatePerpAuctionDuration(new BN(0)); + + console.log('create whitelist mint'); + const whitelistKeypair = Keypair.generate(); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: bankrunContextWrapper.provider.wallet.publicKey, + newAccountPubkey: whitelistKeypair.publicKey, + space: MINT_SIZE, + lamports: 10_000_000_000, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + whitelistKeypair.publicKey, + 0, + bankrunContextWrapper.provider.wallet.publicKey, + bankrunContextWrapper.provider.wallet.publicKey, + TOKEN_PROGRAM_ID + ) + ); + + await bankrunContextWrapper.sendTransaction(transaction, [ + whitelistKeypair, + ]); + + const whitelistMintInfo = + await bankrunContextWrapper.connection.getAccountInfo( + whitelistKeypair.publicKey + ); + console.log('whitelistMintInfo', whitelistMintInfo); + }); + + after(async () => { + await adminClient.unsubscribe(); + for (const userClient of userClients) { + await userClient.unsubscribe(); + } + }); + + it('can create a new LP Pool', async () => { + await adminClient.initializeLpPool( + lpPoolName, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + new Keypair() + ); + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + // check LpPool created + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminClient.wallet.publicKey + ); + + // Check amm constituent map exists + const ammConstituentMapPublicKey = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammConstituentMap = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapPublicKey + )) as AmmConstituentMapping; + expect(ammConstituentMap).to.not.be.null; + assert(ammConstituentMap.weights.length == 0); + + // check constituent target weights exists + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 0); + + // check mint created correctly + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(0); + expect(mintInfo.mintAuthority?.toBase58()).to.equal(lpPoolKey.toBase58()); + }); + + it('can add constituents to LP Pool', async () => { + // USDC Constituent + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [], + }); + + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + await sleep(50); + } + await adminClient.unsubscribe(); + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(adminKeypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1], + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.subscribe(); + await sleep(50); + + const correlations = [ZERO]; + for (let i = 1; i < NUMBER_OF_CONSTITUENTS; i++) { + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: i, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + derivativeWeight: ZERO, + volatility: PERCENTAGE_PRECISION.muln( + Math.floor(Math.random() * 10) + ).divn(100), + constituentCorrelations: correlations, + }); + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.constituents == i + 1); + + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == i + 1); + + correlations.push(new BN(Math.floor(Math.random() * 100)).divn(100)); + } + }); + + it('can initialize many perp markets and given some inventory', async () => { + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + await adminClient.initializePerpMarket( + i, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + new BN(0), + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(i, 1); + await sleep(50); + } + + await adminClient.unsubscribe(); + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(adminKeypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: PERP_MARKET_INDEXES, + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.subscribe(); + }); + + it('can initialize all the different extra users', async () => { + for (let i = 0; i < NUMBER_OF_USERS; i++) { + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + await sleep(100); + const userClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: PERP_MARKET_INDEXES, + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await userClient.subscribe(); + await sleep(100); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + await sleep(100); + + await userClient.initializeUserAccountAndDepositCollateral( + new BN(10_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + await sleep(100); + userClients.push(userClient); + } + + let userIndex = 0; + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + // Give the vamm some inventory + const userClient = userClients[userIndex]; + await userClient.openPosition(PositionDirection.LONG, BASE_PRECISION, i); + await sleep(50); + if ( + userClient + .getUser() + .getActivePerpPositions() + .filter((x) => !x.baseAssetAmount.eq(ZERO)).length == 8 + ) { + userIndex++; + } + } + }); + + it('can add lots of mapping data', async () => { + // Assume that constituent 0 is USDC + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + for (let j = 1; j <= 3; j++) { + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: i, + constituentIndex: j, + weight: PERCENTAGE_PRECISION.divn(3), + }, + ]); + await sleep(50); + } + } + }); + + it('can add all addresses to lookup tables', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + const [lookupTableInst, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: adminClient.wallet.publicKey, + payer: adminClient.wallet.publicKey, + recentSlot: slot.toNumber() - 10, + }); + + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + payer: adminClient.wallet.publicKey, + authority: adminClient.wallet.publicKey, + lookupTable: lookupTableAddress, + addresses: CONSTITUENT_INDEXES.map((i) => + getConstituentPublicKey(program.programId, lpPoolKey, i) + ), + }); + + const tx = new Transaction().add(lookupTableInst).add(extendInstruction); + await adminClient.sendTransaction(tx); + lutAddress = lookupTableAddress; + + const chunkies = chunks( + adminClient.getPerpMarketAccounts().map((account) => account.pubkey), + 20 + ); + for (const chunk of chunkies) { + const extendTx = new Transaction(); + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + payer: adminClient.wallet.publicKey, + authority: adminClient.wallet.publicKey, + lookupTable: lookupTableAddress, + addresses: chunk, + }); + extendTx.add(extendInstruction); + await adminClient.sendTransaction(extendTx); + } + }); + + it('can crank amm info into the cache', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + for (const chunk of chunks(PERP_MARKET_INDEXES, 20)) { + const txSig = await adminClient.updateAmmCache(chunk); + const cus = + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); + console.log(cus); + assert(cus < 200_000); + } + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + expect(ammCache).to.not.be.null; + assert(ammCache.cache.length == NUMBER_OF_PERP_MARKETS); + }); + + it('can update target balances', async () => { + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + } + + const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000, + }); + const ammCacheIxs = await Promise.all( + chunks(PERP_MARKET_INDEXES, 50).map( + async (chunk) => await adminClient.getUpdateAmmCacheIx(chunk) + ) + ); + const updateBaseIx = await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [getConstituentPublicKey(program.programId, lpPoolKey, 1)] + ); + + const txMessage = new TransactionMessage({ + payerKey: adminClient.wallet.publicKey, + recentBlockhash: (await adminClient.connection.getLatestBlockhash()) + .blockhash, + instructions: [cuIx, ...ammCacheIxs, updateBaseIx], + }); + + const lookupTableAccount = ( + await bankrunContextWrapper.connection.getAddressLookupTable(lutAddress) + ).value; + const message = txMessage.compileToV0Message([lookupTableAccount]); + + const txSig = await adminClient.connection.sendTransaction( + new VersionedTransaction(message) + ); + + const cus = Number( + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig) + ); + console.log(cus); + + // assert(+cus.toString() < 100_000); + }); + + it('can update AUM with high balances', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + getConstituentPublicKey(program.programId, lpPoolKey, i), + [['vaultTokenBalance', QUOTE_PRECISION.muln(1000)]] + ); + } + + const tx = new Transaction(); + tx.add( + await adminClient.getUpdateLpPoolAumIxs(lpPool, CONSTITUENT_INDEXES) + ); + const txSig = await adminClient.sendTransaction(tx); + const cus = Number( + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig.txSig) + ); + console.log(cus); + }); +}); + +const chunks = (array: readonly T[], size: number): T[][] => { + return new Array(Math.ceil(array.length / size)) + .fill(null) + .map((_, index) => index * size) + .map((begin) => array.slice(begin, begin + size)); +}; diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts new file mode 100644 index 0000000000..17f9d03c89 --- /dev/null +++ b/tests/lpPoolSwap.ts @@ -0,0 +1,1005 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; +import { Program } from '@coral-xyz/anchor'; +import { + Account, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + encodeName, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + OracleSource, + SPOT_MARKET_RATE_PRECISION, + SPOT_MARKET_WEIGHT_PRECISION, + LPPoolAccount, + convertToNumber, + getConstituentVaultPublicKey, + getConstituentPublicKey, + ConstituentAccount, + ZERO, + getSerumSignerPublicKey, + BN_MAX, + isVariant, + ConstituentStatus, + getSignedTokenAmount, + getTokenAmount, +} from '../sdk/src'; +import { + initializeQuoteSpotMarket, + mockUSDCMint, + mockUserUSDCAccount, + mockOracleNoProgram, + setFeedPriceNoProgram, + overWriteTokenAccountBalance, + overwriteConstituentAccount, + mockAtaTokenAccountForMint, + overWriteMintAccount, + createWSolTokenAccountForUser, + initializeSolSpotMarket, + createUserWithUSDCAndWSOLAccount, + sleep, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { DexInstructions, Market, OpenOrders } from '@project-serum/serum'; +import { listMarket, SERUM, makePlaceOrderTransaction } from './serumHelper'; +import { NATIVE_MINT } from '@solana/spl-token'; +dotenv.config(); + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + + let serumMarketPublicKey: PublicKey; + + let serumDriftClient: TestClient; + let serumWSOL: PublicKey; + let serumUSDC: PublicKey; + let serumKeypair: Keypair; + + let adminSolAta: PublicKey; + + let openOrdersAccount: PublicKey; + + const usdcAmount = new BN(500 * 10 ** 6); + const solAmount = new BN(2 * 10 ** 9); + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const lpPoolName = 'test pool 1'; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey( + program.programId, + encodeName(lpPoolName) + ); + + let userUSDCAccount: Keypair; + let serumMarket: Market; + + before(async () => { + const context = await startAnchor( + '', + [ + { + name: 'serum_dex', + programId: new PublicKey( + 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX' + ), + }, + ], + [] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200.1); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 50 * LAMPORTS_PER_SOL); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1], + spotMarketIndexes: [0, 1, 2], + oracleInfos: [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + new BN(10).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair.publicKey + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(10).mul(QUOTE_PRECISION), + userUSDCAccount.publicKey + ); + + const periodicity = new BN(0); + + await adminClient.initializePerpMarket( + 0, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + 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(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + adminSolAta = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + new BN(20 * 10 ** 9) // 10 SOL + ); + + await adminClient.initializeLpPool( + lpPoolName, + new BN(100), // 1 bps + new BN(100_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() // dlp mint + ); + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION, + volatility: ZERO, + constituentCorrelations: [], + }); + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: PERCENTAGE_PRECISION.muln(4).divn(100), + constituentCorrelations: [ZERO], + }); + + await initializeSolSpotMarket(adminClient, spotMarketOracle); + await adminClient.updateSpotMarketStepSizeAndTickSize( + 2, + new BN(100000000), + new BN(100) + ); + await adminClient.updateSpotAuctionDuration(0); + + await adminClient.deposit( + new BN(5 * 10 ** 9), // 10 SOL + 2, // market index + adminSolAta // user token account + ); + + await adminClient.depositIntoSpotMarketVault( + 2, + new BN(4 * 10 ** 9), // 4 SOL + adminSolAta + ); + + [serumDriftClient, serumWSOL, serumUSDC, serumKeypair] = + await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + program, + solAmount, + usdcAmount, + [], + [0, 1], + [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await bankrunContextWrapper.fundKeypair( + serumKeypair, + 50 * LAMPORTS_PER_SOL + ); + await serumDriftClient.deposit(usdcAmount, 0, serumUSDC); + }); + + after(async () => { + await adminClient.unsubscribe(); + await serumDriftClient.unsubscribe(); + }); + + it('LP Pool init properly', async () => { + let lpPool: LPPoolAccount; + try { + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool).to.not.be.null; + } catch (e) { + expect.fail('LP Pool should have been created'); + } + + try { + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + } catch (e) { + expect.fail('Amm constituent map should have been created'); + } + }); + + it('lp pool swap', async () => { + let spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price1 = convertToNumber(spotOracle.price); + + await setFeedPriceNoProgram(bankrunContextWrapper, 224.3, spotMarketOracle); + + await adminClient.fetchAccounts(); + + spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price2 = convertToNumber(spotOracle.price); + assert(price2 > price1); + + const const0TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const const1TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 1 + ); + + const const0Key = getConstituentPublicKey(program.programId, lpPoolKey, 0); + const const1Key = getConstituentPublicKey(program.programId, lpPoolKey, 1); + + const c0TokenBalance = new BN(224_300_000_000); + const c1TokenBalance = new BN(1_000_000_000); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const0TokenAccount, + BigInt(c0TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const0Key, + [['vaultTokenBalance', c0TokenBalance]] + ); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const1TokenAccount, + BigInt(c1TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const1Key, + [['vaultTokenBalance', c1TokenBalance]] + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + const0Key + )) as ConstituentAccount; + expect(c0.vaultTokenBalance.toString()).to.equal(c0TokenBalance.toString()); + + const c1 = (await adminClient.program.account.constituent.fetch( + const1Key + )) as ConstituentAccount; + expect(c1.vaultTokenBalance.toString()).to.equal(c1TokenBalance.toString()); + + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const prec = new BN(10).pow(new BN(tokenDecimals)); + console.log( + `const0 balance: ${convertToNumber(c0.vaultTokenBalance, prec)}` + ); + console.log( + `const1 balance: ${convertToNumber(c1.vaultTokenBalance, prec)}` + ); + + const lpPool1 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool1.lastAumSlot.toNumber()).to.be.equal(0); + + await adminClient.updateLpPoolAum(lpPool1, [1, 0]); + + const lpPool2 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + expect(lpPool2.lastAumSlot.toNumber()).to.be.greaterThan(0); + expect(lpPool2.lastAum.gt(lpPool1.lastAum)).to.be.true; + console.log(`AUM: ${convertToNumber(lpPool2.lastAum, QUOTE_PRECISION)}`); + + const constituentTargetWeightsPublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + // swap c0 for c1 + + const adminAuth = adminClient.wallet.publicKey; + + // mint some tokens for user + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(224_300_000_000), + adminAuth + ); + const c1UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + spotTokenMint.publicKey, + new BN(1_000_000_000), + adminAuth + ); + + const inTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + + // in = 0, out = 1 + const swapTx = new Transaction(); + swapTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool2, [0, 1])); + swapTx.add( + await adminClient.getLpPoolSwapIx( + 0, + 1, + new BN(224_300_000), + new BN(0), + lpPoolKey, + constituentTargetWeightsPublicKey, + const0TokenAccount, + const1TokenAccount, + c0UserTokenAccount, + c1UserTokenAccount, + const0Key, + const1Key, + usdcMint.publicKey, + spotTokenMint.publicKey + ) + ); + + // Should throw since we havnet enabled swaps yet + try { + await adminClient.sendTransaction(swapTx); + assert(false, 'Should have thrown'); + } catch (error) { + assert(error.message.includes('0x17f1')); + } + + // Enable swaps + await adminClient.updateFeatureBitFlagsSwapLpPool(true); + + // Send swap + await adminClient.sendTransaction(swapTx); + + const inTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + const diffInToken = + inTokenBalanceAfter.amount - inTokenBalanceBefore.amount; + const diffOutToken = + outTokenBalanceAfter.amount - outTokenBalanceBefore.amount; + + expect(Number(diffInToken)).to.be.equal(-224_300_000); + expect(Number(diffOutToken)).to.be.approximately(1001298, 1); + + console.log( + `in Token: ${inTokenBalanceBefore.amount} -> ${ + inTokenBalanceAfter.amount + } (${Number(diffInToken) / 1e6})` + ); + console.log( + `out Token: ${outTokenBalanceBefore.amount} -> ${ + outTokenBalanceAfter.amount + } (${Number(diffOutToken) / 1e6})` + ); + }); + + it('lp pool add and remove liquidity: usdc', async () => { + // add c0 liquidity + const adminAuth = adminClient.wallet.publicKey; + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(1_000_000_000_000), + adminAuth + ); + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumBefore = lpPool.lastAum; + + const userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminAuth + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + const c1 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const userC0TokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tokensAdded = new BN(1_000_000_000_000); + let tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 0, + inAmount: tokensAdded, + minMintAmount: new BN(1), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(tx); + + // Should fail to add more liquidity if it's in redulce only mode; + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.REDUCE_ONLY + ); + tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 0, + inAmount: tokensAdded, + minMintAmount: new BN(1), + lpPool: lpPool, + })) + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.ACTIVE + ); + + await sleep(500); + + const userC0TokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumAfter = lpPool.lastAum; + const lpPoolAumDiff = lpPoolAumAfter.sub(lpPoolAumBefore); + expect(lpPoolAumDiff.toString()).to.be.equal(tokensAdded.toString()); + + const userC0TokenBalanceDiff = + Number(userC0TokenBalanceAfter.amount) - + Number(userC0TokenBalanceBefore.amount); + expect(Number(userC0TokenBalanceDiff)).to.be.equal( + -1 * tokensAdded.toNumber() + ); + + const userLpTokenBalanceDiff = + Number(userLpTokenBalanceAfter.amount) - + Number(userLpTokenBalanceBefore.amount); + expect(userLpTokenBalanceDiff).to.be.equal( + (((tokensAdded.toNumber() * 9997) / 10000) * 9999) / 10000 + ); // max weight deviation: expect min swap% fee on constituent, + 0.01% lp mint fee + + const constituentBalanceBefore = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + console.log(`constituentBalanceBefore: ${constituentBalanceBefore}`); + + // remove liquidity + const removeTx = new Transaction(); + removeTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + removeTx.add( + await adminClient.getDepositToProgramVaultIx( + encodeName(lpPoolName), + 0, + new BN(constituentBalanceBefore) + ) + ); + removeTx.add( + ...(await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(userLpTokenBalanceAfter.amount.toString()), + minAmountOut: new BN(1), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(removeTx); + + const constituentAfterRemoveLiquidity = + (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const blTokenAmountAfterRemoveLiquidity = getSignedTokenAmount( + getTokenAmount( + constituentAfterRemoveLiquidity.spotBalance.scaledBalance, + adminClient.getSpotMarketAccount(0), + constituentAfterRemoveLiquidity.spotBalance.balanceType + ), + constituentAfterRemoveLiquidity.spotBalance.balanceType + ); + + const withdrawFromProgramVaultTx = new Transaction(); + withdrawFromProgramVaultTx.add( + await adminClient.getWithdrawFromProgramVaultIx( + encodeName(lpPoolName), + 0, + blTokenAmountAfterRemoveLiquidity.abs() + ) + ); + await adminClient.sendTransaction(withdrawFromProgramVaultTx); + + const userC0TokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const userC0TokenBalanceAfterBurnDiff = + Number(userC0TokenBalanceAfterBurn.amount) - + Number(userC0TokenBalanceAfter.amount); + + expect(userC0TokenBalanceAfterBurnDiff).to.be.greaterThan(0); + expect(Number(userLpTokenBalanceAfterBurn.amount)).to.be.equal(0); + + const totalC0TokensLost = new BN( + userC0TokenBalanceAfterBurn.amount.toString() + ).sub(tokensAdded); + const totalC0TokensLostPercent = + Number(totalC0TokensLost) / Number(tokensAdded); + expect(totalC0TokensLostPercent).to.be.approximately(-0.0006, 0.0001); // lost about 7bps swapping in an out + }); + + it('Add Serum Market', async () => { + serumMarketPublicKey = await listMarket({ + context: bankrunContextWrapper, + wallet: bankrunContextWrapper.provider.wallet, + baseMint: NATIVE_MINT, + quoteMint: usdcMint.publicKey, + baseLotSize: 100000000, + quoteLotSize: 100, + dexProgramId: SERUM, + feeRateBps: 0, + }); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'confirmed' }, + SERUM + ); + + await adminClient.initializeSerumFulfillmentConfig( + 2, + serumMarketPublicKey, + SERUM + ); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const serumOpenOrdersAccount = new Account(); + const createOpenOrdersIx = await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + serumDriftClient.wallet.publicKey, + serumOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await serumDriftClient.sendTransaction( + new Transaction().add(createOpenOrdersIx), + [serumOpenOrdersAccount] + ); + + const adminOpenOrdersAccount = new Account(); + const adminCreateOpenOrdersIx = + await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + adminClient.wallet.publicKey, + adminOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await adminClient.sendTransaction( + new Transaction().add(adminCreateOpenOrdersIx), + [adminOpenOrdersAccount] + ); + + openOrdersAccount = adminOpenOrdersAccount.publicKey; + }); + + it('swap sol for usdc', async () => { + // Initialize new constituent for market 2 + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION], + }); + + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + console.log(`beforeSOLBalance: ${beforeSOLBalance}`); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + console.log(`beforeUSDCBalance: ${beforeUSDCBalance}`); + + const serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const adminSolAccount = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + ZERO + ); + + // place ask to sell 1 sol for 100 usdc + const { transaction, signers } = await makePlaceOrderTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket, + { + owner: serumDriftClient.wallet, + payer: serumWSOL, + side: 'sell', + price: 100, + size: 1, + orderType: 'postOnly', + clientId: undefined, // todo? + openOrdersAddressKey: undefined, + openOrdersAccount: undefined, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + maxTs: BN_MAX, + } + ); + + const signerKeypairs = signers.map((signer) => { + return Keypair.fromSecretKey(signer.secretKey); + }); + + await serumDriftClient.sendTransaction(transaction, signerKeypairs); + + const amountIn = new BN(200).muln( + 10 ** adminClient.getSpotMarketAccount(0).decimals + ); + + const { beginSwapIx, endSwapIx } = await adminClient.getSwapIx( + { + lpPoolName: encodeName(lpPoolName), + amountIn: amountIn, + inMarketIndex: 0, + outMarketIndex: 2, + inTokenAccount: userUSDCAccount.publicKey, + outTokenAccount: adminSolAccount, + }, + true + ); + + const serumBidIx = serumMarket.makePlaceOrderInstruction( + bankrunContextWrapper.connection.toConnection(), + { + owner: adminClient.wallet.publicKey, + payer: userUSDCAccount.publicKey, + side: 'buy', + price: 100, + size: 2, // larger than maker orders so that entire maker order is taken + orderType: 'ioc', + clientId: new BN(1), // todo? + openOrdersAddressKey: openOrdersAccount, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + } + ); + + const serumConfig = await adminClient.getSerumV3FulfillmentConfig( + serumMarket.publicKey + ); + const settleFundsIx = DexInstructions.settleFunds({ + market: serumMarket.publicKey, + openOrders: openOrdersAccount, + owner: adminClient.wallet.publicKey, + // @ts-ignore + baseVault: serumConfig.serumBaseVault, + // @ts-ignore + quoteVault: serumConfig.serumQuoteVault, + baseWallet: adminSolAccount, + quoteWallet: userUSDCAccount.publicKey, + vaultSigner: getSerumSignerPublicKey( + serumMarket.programId, + serumMarket.publicKey, + serumConfig.serumSignerNonce + ), + programId: serumMarket.programId, + }); + + const tx = new Transaction() + .add(beginSwapIx) + .add(serumBidIx) + .add(settleFundsIx) + .add(endSwapIx); + + // Should fail if usdc is in reduce only + const c0pubkey = getConstituentPublicKey(program.programId, lpPoolKey, 0); + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.REDUCE_ONLY + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.ACTIVE + ); + + const { txSig } = await adminClient.sendTransaction(tx); + + bankrunContextWrapper.printTxLogs(txSig); + + // Balances should be accuarate after swap + const afterSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const afterUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + const solDiff = afterSOLBalance - beforeSOLBalance; + const usdcDiff = afterUSDCBalance - beforeUSDCBalance; + + console.log( + `in Token: ${beforeUSDCBalance} -> ${afterUSDCBalance} (${usdcDiff})` + ); + console.log( + `out Token: ${beforeSOLBalance} -> ${afterSOLBalance} (${solDiff})` + ); + + expect(usdcDiff).to.be.equal(-100040000); + expect(solDiff).to.be.equal(1000000000); + }); + + it('deposit and withdraw atomically before swapping', async () => { + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + await adminClient.depositWithdrawToProgramVault( + encodeName(lpPoolName), + 0, + 2, + new BN(400).mul(QUOTE_PRECISION), // 100 USDC + new BN(2 * 10 ** 9) // 100 USDC + ); + + const afterSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const afterUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + const solDiff = afterSOLBalance - beforeSOLBalance; + const usdcDiff = afterUSDCBalance - beforeUSDCBalance; + + console.log( + `in Token: ${beforeUSDCBalance} -> ${afterUSDCBalance} (${usdcDiff})` + ); + console.log( + `out Token: ${beforeSOLBalance} -> ${afterSOLBalance} (${solDiff})` + ); + + expect(usdcDiff).to.be.equal(-400000000); + expect(solDiff).to.be.equal(2000000000); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + assert(constituent.spotBalance.scaledBalance.eq(new BN(2000000001))); + assert(isVariant(constituent.spotBalance.balanceType, 'borrow')); + }); +}); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index df4e742abe..802c435f43 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -43,7 +43,9 @@ import { PositionDirection, DriftClient, OrderType, -} from '../sdk'; + ConstituentAccount, + SpotMarketAccount, +} from '../sdk/src'; import { TestClient, SPOT_MARKET_RATE_PRECISION, @@ -1205,6 +1207,23 @@ export async function overWritePerpMarket( }); } +export async function overWriteSpotMarket( + driftClient: TestClient, + bankrunContextWrapper: BankrunContextWrapper, + spotMarketKey: PublicKey, + spotMarket: SpotMarketAccount +) { + bankrunContextWrapper.context.setAccount(spotMarketKey, { + executable: false, + owner: driftClient.program.programId, + lamports: LAMPORTS_PER_SOL, + data: await driftClient.program.account.spotMarket.coder.accounts.encode( + 'SpotMarket', + spotMarket + ), + }); +} + export async function getPerpMarketDecoded( driftClient: TestClient, bankrunContextWrapper: BankrunContextWrapper, @@ -1359,3 +1378,29 @@ export async function placeAndFillVammTrade({ console.error(e); } } + +export async function overwriteConstituentAccount( + bankrunContextWrapper: BankrunContextWrapper, + program: Program, + constituentPublicKey: PublicKey, + overwriteFields: Array<[key: keyof ConstituentAccount, value: any]> +) { + const acc = await program.account.constituent.fetch(constituentPublicKey); + if (!acc) { + throw new Error( + `Constituent account ${constituentPublicKey.toBase58()} not found` + ); + } + for (const [key, value] of overwriteFields) { + acc[key] = value; + } + bankrunContextWrapper.context.setAccount(constituentPublicKey, { + executable: false, + owner: program.programId, + lamports: LAMPORTS_PER_SOL, + data: await program.account.constituent.coder.accounts.encode( + 'Constituent', + acc + ), + }); +} From 03b22b8704a3e91ae8f3ee0792904d92a58e4773 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Tue, 16 Sep 2025 20:26:52 -0700 Subject: [PATCH 069/159] add vamm cache percent scalar (default is 100) --- programs/drift/src/instructions/admin.rs | 94 ----------------- programs/drift/src/instructions/lp_admin.rs | 108 +++++++++++++++++++- programs/drift/src/instructions/lp_pool.rs | 6 +- programs/drift/src/state/amm_cache.rs | 6 +- programs/drift/src/state/lp_pool.rs | 1 + sdk/src/adminClient.ts | 9 +- sdk/src/idl/drift.json | 78 ++++++++------ tests/lpPool.ts | 34 +++++- 8 files changed, 197 insertions(+), 139 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 3fc172c225..da41d1fea7 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1114,84 +1114,6 @@ pub fn handle_initialize_amm_cache(ctx: Context) -> Result<( Ok(()) } -pub fn handle_update_initial_amm_cache_info<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, -) -> Result<()> { - let amm_cache = &mut ctx.accounts.amm_cache; - let slot = Clock::get()?.slot; - let state = &ctx.accounts.state; - - let AccountMaps { - perp_market_map, - spot_market_map: _, - mut oracle_map, - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &MarketSet::new(), - &MarketSet::new(), - Clock::get()?.slot, - None, - )?; - - for (_, perp_market_loader) in perp_market_map.0 { - let perp_market = perp_market_loader.load()?; - let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; - let mm_oracle_data = perp_market.get_mm_oracle_price_data( - *oracle_data, - slot, - &ctx.accounts.state.oracle_guard_rails.validity, - )?; - - amm_cache.update_perp_market_fields(&perp_market)?; - amm_cache.update_oracle_info( - slot, - perp_market.market_index, - &mm_oracle_data, - &perp_market, - &state.oracle_guard_rails, - )?; - } - - Ok(()) -} -#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] -pub struct OverrideAmmCacheParams { - pub quote_owed_from_lp_pool: Option, - pub last_settle_slot: Option, - pub last_fee_pool_token_amount: Option, - pub last_net_pnl_pool_token_amount: Option, -} - -pub fn handle_override_amm_cache_info<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, - market_index: u16, - override_params: OverrideAmmCacheParams, -) -> Result<()> { - let amm_cache = &mut ctx.accounts.amm_cache; - - let cache_entry = amm_cache.cache.get_mut(market_index as usize); - if cache_entry.is_none() { - msg!("No cache entry found for market index {}", market_index); - return Ok(()); - } - - let cache_entry = cache_entry.unwrap(); - if let Some(quote_owed_from_lp_pool) = override_params.quote_owed_from_lp_pool { - cache_entry.quote_owed_from_lp_pool = quote_owed_from_lp_pool; - } - if let Some(last_settle_slot) = override_params.last_settle_slot { - cache_entry.last_settle_slot = last_settle_slot; - } - if let Some(last_fee_pool_token_amount) = override_params.last_fee_pool_token_amount { - cache_entry.last_fee_pool_token_amount = last_fee_pool_token_amount; - } - if let Some(last_net_pnl_pool_token_amount) = override_params.last_net_pnl_pool_token_amount { - cache_entry.last_net_pnl_pool_token_amount = last_net_pnl_pool_token_amount; - } - - Ok(()) -} - #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -5421,22 +5343,6 @@ pub struct InitializeAmmCache<'info> { pub system_program: Program<'info, System>, } -#[derive(Accounts)] -pub struct UpdateInitialAmmCacheInfo<'info> { - #[account( - mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin - )] - pub state: Box>, - pub admin: Signer<'info>, - #[account( - mut, - seeds = [AMM_POSITIONS_CACHE.as_ref()], - bump = amm_cache.bump, - )] - pub amm_cache: Box>, -} - #[derive(Accounts)] pub struct DeleteInitializedPerpMarket<'info> { #[account(mut)] diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 081bde1604..114064753a 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1,11 +1,12 @@ +use crate::state::perp_market_map::MarketSet; use crate::{controller, load_mut}; use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; use crate::error::ErrorCode; use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet}; -use crate::instructions::optional_accounts::get_token_mint; +use crate::instructions::optional_accounts::{get_token_mint, load_maps, AccountMaps}; use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; use crate::math::safe_math::SafeMath; -use crate::state::amm_cache::AmmCache; +use crate::state::amm_cache::{AmmCache, AMM_POSITIONS_CACHE}; use crate::state::lp_pool::{ AmmConstituentDatum, AmmConstituentMapping, Constituent, ConstituentCorrelations, ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, @@ -896,6 +897,90 @@ pub fn handle_update_perp_market_lp_pool_status( Ok(()) } +pub fn handle_update_initial_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + + for (_, perp_market_loader) in perp_market_map.0 { + let perp_market = perp_market_loader.load()?; + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + let mm_oracle_data = perp_market.get_mm_oracle_price_data( + *oracle_data, + slot, + &ctx.accounts.state.oracle_guard_rails.validity, + )?; + + amm_cache.update_perp_market_fields(&perp_market)?; + amm_cache.update_oracle_info( + slot, + perp_market.market_index, + &mm_oracle_data, + &perp_market, + &state.oracle_guard_rails, + )?; + } + + Ok(()) +} +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +pub struct OverrideAmmCacheParams { + pub quote_owed_from_lp_pool: Option, + pub last_settle_slot: Option, + pub last_fee_pool_token_amount: Option, + pub last_net_pnl_pool_token_amount: Option, + pub amm_position_scalar: Option, +} + +pub fn handle_override_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + market_index: u16, + override_params: OverrideAmmCacheParams, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + + let cache_entry = amm_cache.cache.get_mut(market_index as usize); + if cache_entry.is_none() { + msg!("No cache entry found for market index {}", market_index); + return Ok(()); + } + + let cache_entry = cache_entry.unwrap(); + if let Some(quote_owed_from_lp_pool) = override_params.quote_owed_from_lp_pool { + cache_entry.quote_owed_from_lp_pool = quote_owed_from_lp_pool; + } + if let Some(last_settle_slot) = override_params.last_settle_slot { + cache_entry.last_settle_slot = last_settle_slot; + } + if let Some(last_fee_pool_token_amount) = override_params.last_fee_pool_token_amount { + cache_entry.last_fee_pool_token_amount = last_fee_pool_token_amount; + } + if let Some(last_net_pnl_pool_token_amount) = override_params.last_net_pnl_pool_token_amount { + cache_entry.last_net_pnl_pool_token_amount = last_net_pnl_pool_token_amount; + } + + if let Some(amm_position_scalar) = override_params.amm_position_scalar { + cache_entry.amm_position_scalar = amm_position_scalar; + } + + Ok(()) +} + + #[derive(Accounts)] #[instruction( name: [u8; 32], @@ -1252,6 +1337,23 @@ pub struct UpdatePerpMarketLpPoolStatus<'info> { pub state: Box>, #[account(mut)] pub perp_market: AccountLoader<'info, PerpMarket>, - #[account(mut)] + #[account(mut, seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump,)] + pub amm_cache: Box>, +} + +#[derive(Accounts)] +pub struct UpdateInitialAmmCacheInfo<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub state: Box>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] pub amm_cache: Box>, } diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 884171756d..3ec3ae587b 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -129,10 +129,14 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( } amm_inventories.push(AmmInventoryAndPrices { - inventory: cache_info.position, + inventory: cache_info + .position + .safe_mul(cache_info.amm_position_scalar as i64)? + .safe_div(100)?, price: cache_info.oracle_price, }); } + msg!("amm inventories: {:?}", amm_inventories); if amm_inventories.is_empty() { msg!("No valid inventories found for constituent target weights update"); diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index f17e88cc96..5b153f7e46 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -55,7 +55,8 @@ pub struct CacheInfo { pub oracle_source: u8, pub oracle_validity: u8, pub lp_status_for_perp_market: u8, - pub _padding: [u8; 13], + pub amm_position_scalar: u8, + pub _padding: [u8; 12], } impl Size for CacheInfo { @@ -82,7 +83,8 @@ impl Default for CacheInfo { oracle_source: 0u8, quote_owed_from_lp_pool: 0i64, lp_status_for_perp_market: 0u8, - _padding: [0u8; 13], + amm_position_scalar: 100u8, + _padding: [0u8; 12], } } } diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 9552036597..6df71b7a13 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1187,6 +1187,7 @@ pub fn calculate_target_weight( } /// Update target base based on amm_inventory and mapping +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AmmInventoryAndPrices { pub inventory: i64, pub price: i64, diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 9556cf9fc8..efe50f5b4c 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -712,6 +712,7 @@ export class AdminClient extends DriftClient { lastSettleTs?: BN; lastFeePoolTokenAmount?: BN; lastNetPnlPoolTokenAmount?: BN; + ammPositionScalar?: number; }, txParams?: TxParams ): Promise { @@ -730,20 +731,22 @@ export class AdminClient extends DriftClient { perpMarketIndex: number, params: { quoteOwedFromLpPool?: BN; - lastSettleTs?: BN; + lastSettleSlot?: BN; lastFeePoolTokenAmount?: BN; lastNetPnlPoolTokenAmount?: BN; + ammPositionScalar?: number; } ): Promise { - return await this.program.instruction.overrideAmmCacheInfo( + return this.program.instruction.overrideAmmCacheInfo( perpMarketIndex, Object.assign( {}, { quoteOwedFromLpPool: null, - lastSettleTs: null, + lastSettleSlot: null, lastFeePoolTokenAmount: null, lastNetPnlPoolTokenAmount: null, + ammPositionScalar: null, }, params ), diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 64bc5d6f40..86775ca67c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -11731,38 +11731,6 @@ } ], "types": [ - { - "name": "OverrideAmmCacheParams", - "type": { - "kind": "struct", - "fields": [ - { - "name": "quoteOwedFromLpPool", - "type": { - "option": "i64" - } - }, - { - "name": "lastSettleSlot", - "type": { - "option": "u64" - } - }, - { - "name": "lastFeePoolTokenAmount", - "type": { - "option": "u128" - } - }, - { - "name": "lastNetPnlPoolTokenAmount", - "type": { - "option": "i128" - } - } - ] - } - }, { "name": "UpdatePerpMarketSummaryStatsParams", "type": { @@ -11913,6 +11881,44 @@ ] } }, + { + "name": "OverrideAmmCacheParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteOwedFromLpPool", + "type": { + "option": "i64" + } + }, + { + "name": "lastSettleSlot", + "type": { + "option": "u64" + } + }, + { + "name": "lastFeePoolTokenAmount", + "type": { + "option": "u128" + } + }, + { + "name": "lastNetPnlPoolTokenAmount", + "type": { + "option": "i128" + } + }, + { + "name": "ammPositionScalar", + "type": { + "option": "u8" + } + } + ] + } + }, { "name": "AddAmmConstituentMappingDatum", "type": { @@ -12009,12 +12015,16 @@ "name": "lpStatusForPerpMarket", "type": "u8" }, + { + "name": "ammPositionScalar", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 13 + 12 ] } } @@ -19115,4 +19125,4 @@ "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} +} \ No newline at end of file diff --git a/tests/lpPool.ts b/tests/lpPool.ts index bc8a141023..d4361ea204 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -721,7 +721,7 @@ describe('LP Pool', () => { ); await adminClient.updateAmmCache([0, 1, 2]); - const tx = new Transaction(); + let tx = new Transaction(); tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); tx.add( await adminClient.getUpdateLpConstituentTargetBaseIx( @@ -738,7 +738,7 @@ describe('LP Pool', () => { program.programId, lpPoolKey ); - const constituentTargetBase = + let constituentTargetBase = (await adminClient.program.account.constituentTargetBase.fetch( constituentTargetBasePublicKey )) as ConstituentTargetBaseAccount; @@ -748,6 +748,36 @@ describe('LP Pool', () => { constituentTargetBase.targets.filter((x) => x.targetBase.eq(ZERO)) .length !== constituentTargetBase.targets.length ); + + // Make sure the target base respects the cache scalar + const cacheValueBefore = constituentTargetBase.targets[1].targetBase; + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 50, + }); + tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + tx.add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ] + ) + ); + await adminClient.sendTransaction(tx); + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + console.log(cacheValueBefore.toString()); + expect( + constituentTargetBase.targets[1].targetBase.toNumber() + ).to.approximately(cacheValueBefore.muln(50).divn(100).toNumber(), 1); + + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 100, + }); }); it('can add constituent to LP Pool thats a derivative and behave correctly', async () => { From a2a0c5286ad9c34e69b5117f5d48fdabd15b073e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 17 Sep 2025 10:51:54 -0400 Subject: [PATCH 070/159] program: add auto cancel reduce only tpsl --- programs/drift/src/controller/orders.rs | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6cff2d1350..32484ae60b 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1379,6 +1379,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)); } @@ -2927,6 +2941,53 @@ 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].reduce_only { + continue; + } + + if user.orders[order_index].market_index != perp_market_index { + continue; + } + + if !user.orders[order_index].must_be_triggered() { + 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, From db517d945185264926f194f566ed7b09657d3910 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 17 Sep 2025 15:58:02 -0400 Subject: [PATCH 071/159] cargo test --- programs/drift/src/controller/orders.rs | 6 +- programs/drift/src/controller/orders/tests.rs | 223 ++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 32484ae60b..669952732f 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -2957,7 +2957,7 @@ fn cancel_reduce_only_trigger_orders( continue; } - if !user.orders[order_index].reduce_only { + if user.orders[order_index].market_type != MarketType::Perp { continue; } @@ -2969,6 +2969,10 @@ fn cancel_reduce_only_trigger_orders( continue; } + if !user.orders[order_index].reduce_only { + continue; + } + cancel_order( order_index, user, diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index b5e681f090..c79fe78808 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -10315,6 +10315,229 @@ 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; From 84803e616d7bac20ad839e428f2ceee88225ddbb Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 17 Sep 2025 16:01:38 -0400 Subject: [PATCH 072/159] cargo fmt -- --- programs/drift/src/controller/orders/tests.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index c79fe78808..840cfdb9c9 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -10325,9 +10325,9 @@ pub mod cancel_reduce_only_trigger_orders { 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, + 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; @@ -10534,7 +10534,6 @@ pub mod cancel_reduce_only_trigger_orders { assert_eq!(user.orders[2].status, OrderStatus::Canceled); assert_eq!(user.orders[3].status, OrderStatus::Open); assert_eq!(user.orders[4].status, OrderStatus::Canceled); - } } From dc433969fdcdbc887367d581a01331d412dfd326 Mon Sep 17 00:00:00 2001 From: moosecat Date: Wed, 17 Sep 2025 13:51:28 -0700 Subject: [PATCH 073/159] aum cant go below zero (#1890) * aum cant go below zero * keep it signed --- programs/drift/src/instructions/lp_admin.rs | 9 +++---- programs/drift/src/instructions/lp_pool.rs | 27 ++++++++++----------- programs/drift/src/math/lp_pool.rs | 5 +++- programs/drift/src/state/lp_pool.rs | 26 +++++++++++++++----- programs/drift/src/state/lp_pool/tests.rs | 4 +-- tests/lpPool.ts | 24 +++++++++--------- tests/lpPoolCUs.ts | 6 ++--- 7 files changed, 58 insertions(+), 43 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 114064753a..72b3333932 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1,5 +1,3 @@ -use crate::state::perp_market_map::MarketSet; -use crate::{controller, load_mut}; use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; use crate::error::ErrorCode; use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet}; @@ -14,9 +12,11 @@ use crate::state::lp_pool::{ CONSTITUENT_VAULT_PDA_SEED, }; use crate::state::perp_market::PerpMarket; +use crate::state::perp_market_map::MarketSet; use crate::state::spot_market::SpotMarket; use crate::state::state::State; use crate::validate; +use crate::{controller, load_mut}; use anchor_lang::prelude::*; use anchor_lang::Discriminator; use anchor_spl::associated_token::AssociatedToken; @@ -893,7 +893,7 @@ pub fn handle_update_perp_market_lp_pool_status( msg!("perp market {}", perp_market.market_index); perp_market.lp_status = lp_status; amm_cache.update_perp_market_fields(&perp_market)?; - + Ok(()) } @@ -980,7 +980,6 @@ pub fn handle_override_amm_cache_info<'c: 'info, 'info>( Ok(()) } - #[derive(Accounts)] #[instruction( name: [u8; 32], @@ -1136,7 +1135,7 @@ pub struct UpdateConstituentParams<'info> { pub struct UpdateConstituentStatus<'info> { #[account( mut, - constraint = admin.key() == state.admin + constraint = admin.key() == state.admin )] pub admin: Signer<'info>, pub state: Box>, diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 3ec3ae587b..57e5ba742b 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -220,6 +220,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( )?; // Set USDC stable weight + msg!("aum: {}", aum); let total_stable_target_base = aum .cast::()? .safe_sub(crypto_delta.abs())? @@ -808,16 +809,15 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( ctx.accounts.lp_mint.reload()?; let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; - if lp_price_before != 0 { - let price_diff_percent = lp_price_after - .abs_diff(lp_price_before) - .safe_mul(PERCENTAGE_PRECISION)? - .safe_div(lp_price_before)?; + let price_diff = (lp_price_after.cast::()?).safe_sub(lp_price_before.cast::()?)?; + if lp_price_before > 0 && price_diff.signum() != 0 && in_fee_amount.signum() != 0 { validate!( - price_diff_percent <= PERCENTAGE_PRECISION / 5, + price_diff.signum() == in_fee_amount.signum() || price_diff == 0, ErrorCode::LpInvariantFailed, - "Removing liquidity resulted in DLP token difference of > 5%" + "Adding liquidity resulted in price direction != fee sign, price_diff: {}, in_fee_amount: {}", + price_diff, + in_fee_amount )?; } @@ -1192,15 +1192,14 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( ctx.accounts.lp_mint.reload()?; let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; - if lp_price_after != 0 { - let price_diff_percent = lp_price_after - .abs_diff(lp_price_before) - .safe_mul(PERCENTAGE_PRECISION)? - .safe_div(lp_price_before)?; + let price_diff = (lp_price_after.cast::()?).safe_sub(lp_price_before.cast::()?)?; + if price_diff.signum() != 0 && out_fee_amount.signum() != 0 { validate!( - price_diff_percent <= PERCENTAGE_PRECISION / 5, + price_diff.signum() == out_fee_amount.signum(), ErrorCode::LpInvariantFailed, - "Removing liquidity resulted in DLP token difference of > 5%" + "Removing liquidity resulted in price direction != fee sign, price_diff: {}, out_fee_amount: {}", + price_diff, + out_fee_amount )?; } diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs index 145442e395..f730ba5239 100644 --- a/programs/drift/src/math/lp_pool.rs +++ b/programs/drift/src/math/lp_pool.rs @@ -79,7 +79,10 @@ pub mod perp_lp_pool_settlement { let amount_to_send = ctx .quote_owed_from_lp .cast::()? - .min(ctx.quote_constituent_token_balance) + .min( + ctx.quote_constituent_token_balance + .saturating_sub(QUOTE_PRECISION_U64), + ) .min(ctx.max_settle_quote_amount); Ok(SettlementResult { diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 6df71b7a13..debd5adb35 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -4,7 +4,7 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::constants::{ BASE_PRECISION_I128, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, }; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; @@ -300,13 +300,17 @@ impl LPPool { &self, out_spot_market: &SpotMarket, out_constituent: &Constituent, - lp_burn_amount: u64, + lp_to_burn: u64, out_oracle: &OraclePriceData, out_target_weight: i64, dlp_total_supply: u64, ) -> DriftResult<(u64, u128, i64, i128)> { let lp_fee_to_charge_pct = self.min_mint_fee; - // let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, false)?; + let mut lp_burn_amount = lp_to_burn; + if dlp_total_supply.saturating_sub(lp_burn_amount) <= QUOTE_PRECISION_U64 { + lp_burn_amount = dlp_total_supply.saturating_sub(QUOTE_PRECISION_U64); + } + let lp_fee_to_charge = lp_burn_amount .cast::()? .safe_mul(lp_fee_to_charge_pct.cast::()?)? @@ -676,16 +680,26 @@ impl LPPool { .cast::()?; crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?; } - aum = aum.safe_add(constituent_aum)?; + aum = aum.saturating_add(constituent_aum); } msg!("Aum before quote owed from lp pool: {}", aum); + let mut total_quote_owed: i128 = 0; for cache_datum in amm_cache.iter() { - aum = aum.saturating_sub(cache_datum.quote_owed_from_lp_pool as i128); + total_quote_owed = + total_quote_owed.safe_add(cache_datum.quote_owed_from_lp_pool as i128)?; + } + + if total_quote_owed > 0 { + aum = aum + .saturating_sub(total_quote_owed as i128) + .max(QUOTE_PRECISION_I128); + } else if total_quote_owed < 0 { + aum = aum.saturating_add((-total_quote_owed) as i128); } - let aum_u128 = aum.max(0i128).cast::()?; + let aum_u128 = aum.max(0).cast::()?; self.last_aum = aum_u128; self.last_aum_slot = slot; diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index f872dd7616..19e72d8a03 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -432,7 +432,7 @@ mod tests { let mut constituents_indexes_and_decimals_and_prices = vec![ConstituentIndexAndDecimalAndPrice { constituent_index: 1, - decimals: 6, + decimals: 9, price: 142_000_000, }]; @@ -459,7 +459,7 @@ mod tests { .unwrap(); assert_eq!(target_zc_mut.len(), 1); - assert_eq!(target_zc_mut.get(0).target_base, -1_000); // despite no aum, desire to reach target + assert_eq!(target_zc_mut.get(0).target_base, -1_000_000); // despite no aum, desire to reach target assert_eq!(target_zc_mut.get(0).last_slot, now_ts); } } diff --git a/tests/lpPool.ts b/tests/lpPool.ts index d4361ea204..0424df1d6b 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -685,6 +685,7 @@ describe('LP Pool', () => { lpPoolKey )) as LPPoolAccount; + console.log(lpPool.lastAum.toString()); assert(lpPool.lastAum.eq(new BN(1000).mul(QUOTE_PRECISION))); // Should fail if we dont pass in the second constituent @@ -1218,18 +1219,18 @@ describe('LP Pool', () => { constituentVaultPublicKey ); - // Should have written fee pool amount owed to the amm cache and new constituent usdc balane should be 0 + // Should have written fee pool amount owed to the amm cache and new constituent usdc balane should just be the quote precision to leave aum > 0 ammCache = (await adminClient.program.account.ammCache.fetch( getAmmCachePublicKey(program.programId) )) as AmmCache; // No more usdc left in the constituent vault - assert(constituent.vaultTokenBalance.eq(ZERO)); - assert(new BN(constituentVault.amount.toString()).eq(ZERO)); + assert(constituent.vaultTokenBalance.eq(QUOTE_PRECISION)); + assert(new BN(constituentVault.amount.toString()).eq(QUOTE_PRECISION)); // Should have recorded the amount left over to the amm cache and increased the amount in the fee pool assert( ammCache.cache[0].lastFeePoolTokenAmount.eq( - new BN(constituentUSDCBalanceBefore.toString()) + new BN(constituentUSDCBalanceBefore.toString()).sub(QUOTE_PRECISION) ) ); expect( @@ -1237,6 +1238,7 @@ describe('LP Pool', () => { ).to.be.approximately( expectedTransferAmount .sub(new BN(constituentUSDCBalanceBefore.toString())) + .add(QUOTE_PRECISION) .toNumber(), 1 ); @@ -1244,9 +1246,9 @@ describe('LP Pool', () => { adminClient .getPerpMarketAccount(0) .amm.feePool.scaledBalance.eq( - new BN(constituentUSDCBalanceBefore.toString()).mul( - SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION) - ) + new BN(constituentUSDCBalanceBefore.toString()) + .sub(QUOTE_PRECISION) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) ) ); @@ -1255,7 +1257,7 @@ describe('LP Pool', () => { lpPool = (await adminClient.program.account.lpPool.fetch( lpPoolKey )) as LPPoolAccount; - assert(lpPool.lastAum.eq(ZERO)); + assert(lpPool.lastAum.eq(QUOTE_PRECISION)); }); it('perp market will not transfer with the constituent vault if it is owed from dlp', async () => { @@ -1303,8 +1305,8 @@ describe('LP Pool', () => { expect( ammCache.cache[0].quoteOwedFromLpPool.toNumber() ).to.be.approximately(owedAmount.divn(2).toNumber(), 1); - assert(constituent.vaultTokenBalance.eq(ZERO)); - assert(lpPool.lastAum.eq(ZERO)); + assert(constituent.vaultTokenBalance.eq(QUOTE_PRECISION)); + assert(lpPool.lastAum.eq(QUOTE_PRECISION)); // Deposit here to DLP to make sure aum calc work with perp market debt await overWriteMintAccount( @@ -1368,7 +1370,7 @@ describe('LP Pool', () => { const balanceBefore = constituent.vaultTokenBalance; const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; - // Give the perp market half of its owed amount + // Give the perp market double of its owed amount const perpMarket = adminClient.getPerpMarketAccount(0); perpMarket.amm.feePool.scaledBalance = perpMarket.amm.feePool.scaledBalance.add( diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index 831335fa36..adbf86979e 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -103,11 +103,10 @@ describe('LP Pool', () => { let bankrunContextWrapper: BankrunContextWrapper; let bulkAccountLoader: TestBulkAccountLoader; - let userLpTokenAccount: PublicKey; + let _userLpTokenAccount: PublicKey; let adminClient: TestClient; let usdcMint: Keypair; let spotTokenMint: Keypair; - let spotMarketOracle: PublicKey; let spotMarketOracle2: PublicKey; let adminKeypair: Keypair; @@ -166,7 +165,6 @@ describe('LP Pool', () => { usdcMint = await mockUSDCMint(bankrunContextWrapper); spotTokenMint = await mockUSDCMint(bankrunContextWrapper); - spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200); spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); const keypair = new Keypair(); @@ -267,7 +265,7 @@ describe('LP Pool', () => { lpPoolKey )) as LPPoolAccount; - userLpTokenAccount = await mockAtaTokenAccountForMint( + _userLpTokenAccount = await mockAtaTokenAccountForMint( bankrunContextWrapper, lpPool.mint, new BN(0), From d04e6fc174c7c65c441b62ab3d310e629f4dee37 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 17 Sep 2025 17:18:43 -0400 Subject: [PATCH 074/159] program: let anotehr signer deposit on user behalf --- programs/drift/src/instructions/admin.rs | 1 + programs/drift/src/instructions/user.rs | 16 ++++++++++++++-- programs/drift/src/state/events.rs | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index a4ecc2f8d1..cfdca1810a 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4764,6 +4764,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/user.rs b/programs/drift/src/instructions/user.rs index e6d13d6fec..db9578d669 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -653,6 +653,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, @@ -670,6 +675,7 @@ pub fn handle_deposit<'c: 'info, 'info>( market_index, explanation, transfer_user: None, + signer, }; emit!(deposit_record); @@ -825,6 +831,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); @@ -995,6 +1002,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); } @@ -1059,6 +1067,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); } @@ -1271,6 +1280,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); @@ -1305,6 +1315,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); } @@ -1371,6 +1382,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); @@ -1405,6 +1417,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); } @@ -4140,8 +4153,7 @@ pub struct InitializeReferrerName<'info> { pub struct Deposit<'info> { pub state: Box>, #[account( - mut, - constraint = can_sign_for_user(&user, &authority)? + mut )] pub user: AccountLoader<'info, User>, #[account( diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index f4806fa5be..d193e17c83 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)] From 497638ddec240fac29d030e6147f1820bc708424 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Wed, 17 Sep 2025 20:24:51 -0700 Subject: [PATCH 075/159] reassing dlp taker bot wallet --- programs/drift/src/ids.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index df2415d936..e651b1b4b3 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -113,5 +113,5 @@ pub mod amm_spread_adjust_wallet { pub mod lp_pool_swap_wallet { use solana_program::declare_id; - declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); + declare_id!("25qbsE2oWri76c9a86ubn17NKKdo6Am4HXD2Jm8vT8K4"); } From 6976e55c32957ff9704108ea8c292d91fef43da1 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Thu, 18 Sep 2025 08:56:41 -0700 Subject: [PATCH 076/159] add event subscriber changes for dlp events --- sdk/src/events/types.ts | 14 +++++++++++++- sdk/src/types.ts | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/sdk/src/events/types.ts b/sdk/src/events/types.ts index 2671344ceb..e217e807da 100644 --- a/sdk/src/events/types.ts +++ b/sdk/src/events/types.ts @@ -21,6 +21,9 @@ import { FuelSeasonRecord, InsuranceFundSwapRecord, TransferProtocolIfSharesToRevenuePoolRecord, + LPSettleRecord, + LPSwapRecord, + LPMintRedeemRecord, } from '../types'; import { EventEmitter } from 'events'; @@ -61,6 +64,9 @@ export const DefaultEventSubscriptionOptions: EventSubscriptionOptions = { 'FuelSeasonRecord', 'InsuranceFundSwapRecord', 'TransferProtocolIfSharesToRevenuePoolRecord', + 'LPSettleRecord', + 'LPMintRedeemRecord', + 'LPSwapRecord', ], maxEventsPerType: 4096, orderBy: 'blockchain', @@ -110,6 +116,9 @@ export type EventMap = { FuelSeasonRecord: Event; InsuranceFundSwapRecord: Event; TransferProtocolIfSharesToRevenuePoolRecord: Event; + LPSettleRecord: Event; + LPMintRedeemRecord: Event; + LPSwapRecord: Event; }; export type EventType = keyof EventMap; @@ -135,7 +144,10 @@ export type DriftEvent = | Event | Event | Event - | Event; + | Event + | Event + | Event + | Event; export interface EventSubscriberEvents { newEvent: (event: WrappedEvent) => void; diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 763ca15cd8..1d3f4aad74 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -771,7 +771,7 @@ export type LPMintRedeemRecord = { ts: BN; slot: BN; authority: PublicKey; - isMinting: boolean; + description: number; amount: BN; fee: BN; spotMarketIndex: number; @@ -789,6 +789,20 @@ export type LPMintRedeemRecord = { inMarketTargetWeight: BN; }; +export type LPSettleRecord = { + recordId: BN; + lastTs: BN; + lastSlot: BN; + ts: BN; + slot: BN; + perpMarketIndex: number; + settleToLpAmount: BN; + perpAmmPnlDelta: BN; + perpAmmExFeeDelta: BN; + lpAum: BN; + lpPrice: BN; +}; + export type StateAccount = { admin: PublicKey; exchangeStatus: number; From 9d7a2b6bbc97a7b52bdfa57384b52f55f8254526 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Fri, 19 Sep 2025 09:48:49 -0700 Subject: [PATCH 077/159] give permission for dlp taker bot to deposit withdraw from program vault to dlp --- programs/drift/src/instructions/lp_admin.rs | 5 ++++- programs/drift/src/instructions/lp_pool.rs | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 72b3333932..71f271d0bb 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1277,7 +1277,10 @@ pub struct UpdateConstituentCorrelation<'info> { )] pub struct LPTakerSwap<'info> { pub state: Box>, - #[account(mut)] + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == lp_pool_swap_wallet::id() + )] pub admin: Signer<'info>, /// Signer token accounts #[account( diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 57e5ba742b..7971aeb26c 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -1,6 +1,7 @@ use anchor_lang::{prelude::*, Accounts, Key, Result}; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use crate::ids::lp_pool_swap_wallet; use crate::math::constants::{PERCENTAGE_PRECISION, PRICE_PRECISION_I64}; use crate::math::oracle::OracleValidity; use crate::state::paused_operations::ConstituentLpOperation; @@ -1600,7 +1601,7 @@ pub struct DepositProgramVault<'info> { pub state: Box>, #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin || admin.key() == lp_pool_swap_wallet::id() )] pub admin: Signer<'info>, #[account(mut)] @@ -1636,7 +1637,7 @@ pub struct WithdrawProgramVault<'info> { pub state: Box>, #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin || admin.key() == lp_pool_swap_wallet::id() )] pub admin: Signer<'info>, /// CHECK: program signer From a814f7bf8ddd34723752e3a29023cf230f12ab7e Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Fri, 19 Sep 2025 11:27:13 -0700 Subject: [PATCH 078/159] fix max withdrawals bug --- programs/drift/src/instructions/lp_admin.rs | 2 +- programs/drift/src/state/lp_pool.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 71f271d0bb..58f3e18543 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1279,7 +1279,7 @@ pub struct LPTakerSwap<'info> { pub state: Box>, #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == lp_pool_swap_wallet::id() + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == lp_pool_swap_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, /// Signer token accounts diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index debd5adb35..6a23963cd4 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -978,13 +978,12 @@ impl Constituent { pub fn get_max_transfer(&self, spot_market: &SpotMarket) -> DriftResult { let token_amount = self.get_full_token_amount(spot_market)?; - - let max_transfer = if self.spot_balance.balance_type == SpotBalanceType::Borrow { + let max_transfer = if token_amount < 0 { self.max_borrow_token_amount - .saturating_sub(token_amount as u64) + .saturating_sub(token_amount.cast::()?) } else { self.max_borrow_token_amount - .saturating_add(token_amount as u64) + .saturating_add(token_amount.cast::()?) }; Ok(max_transfer) From ba10482810ad506316ea679987325a03acabc2f5 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 20 Sep 2025 09:52:00 -0400 Subject: [PATCH 079/159] fix broken cargo tests --- programs/drift/src/controller/liquidation.rs | 7 +++++++ programs/drift/src/controller/liquidation/tests.rs | 2 ++ programs/drift/src/state/user.rs | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 59ee55c86d..514a9cf97e 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3753,9 +3753,11 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; + 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)?; } @@ -3765,9 +3767,14 @@ pub fn set_user_status_to_being_liquidated( 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); + } + Ok(()) } diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index bbd15d1b2d..9ffe22c0b2 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -10465,6 +10465,7 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { 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() }, @@ -10594,6 +10595,7 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { 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, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 0535b2ffad..69c2773ffd 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1112,7 +1112,7 @@ impl PerpPosition { } pub fn is_available(&self) -> bool { - !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() && self.isolated_position_scaled_balance == 0 + !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 { From 58df2ffc8df82ff3e8158330eceaa79693545bb9 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 20 Sep 2025 09:56:30 -0400 Subject: [PATCH 080/159] cargo fmt -- --- programs/drift/src/controller/amm.rs | 13 +- programs/drift/src/controller/amm/tests.rs | 230 +++++++++++++++--- .../drift/src/controller/isolated_position.rs | 20 +- .../src/controller/isolated_position/tests.rs | 90 ++++--- .../drift/src/controller/liquidation/tests.rs | 112 ++++++--- programs/drift/src/controller/pnl.rs | 7 +- programs/drift/src/controller/pnl/tests.rs | 6 +- .../src/controller/spot_balance/tests.rs | 12 +- programs/drift/src/math/margin.rs | 11 +- programs/drift/src/math/margin/tests.rs | 2 +- programs/drift/src/state/user.rs | 6 +- 11 files changed, 370 insertions(+), 139 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 1d8ae06409..93a190a4c7 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -15,8 +15,8 @@ use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ - CONCENTRATION_PRECISION, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, - K_BPS_UPDATE_SCALE, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, + CONCENTRATION_PRECISION, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, K_BPS_UPDATE_SCALE, + MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, }; use crate::math::cp_curve::get_update_k_result; use crate::math::repeg::get_total_fee_lower_bound; @@ -726,9 +726,12 @@ pub fn update_pool_balances( min(user_unsettled_pnl, pnl_pool_token_amount.cast::()?) } else { // 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, user_quote_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) }; diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index 4c58d86f1e..5147d85ef2 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -387,13 +387,25 @@ fn update_pool_balances_test() { let spot_position = SpotPosition::default(); 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(); + 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 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(); + 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); @@ -413,8 +425,14 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 1); 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(); + 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(), @@ -433,7 +451,14 @@ fn update_pool_balances_test() { market.amm.total_fee_minus_distributions = 0; 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(); + 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, @@ -577,7 +602,14 @@ fn update_pool_balances_fee_to_revenue_test() { let spot_position = SpotPosition::default(); 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(); + 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); @@ -593,7 +625,14 @@ fn update_pool_balances_fee_to_revenue_test() { (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; 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(); + 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); @@ -606,13 +645,27 @@ fn update_pool_balances_fee_to_revenue_test() { market.insurance_claim.quote_max_insurance = 1; // add min insurance 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(); + 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 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(); + 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); } @@ -692,7 +745,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { let spot_position = SpotPosition::default(); 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(); + 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); @@ -708,7 +768,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; 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(); + 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); @@ -723,7 +790,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { market.amm.net_revenue_since_last_funding = 1; 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(); + 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); @@ -731,7 +805,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { market.amm.net_revenue_since_last_funding = 100000000; 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(); + 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); } @@ -828,7 +909,14 @@ fn update_pool_balances_revenue_to_fee_test() { ); 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(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -860,7 +948,14 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.amm.total_fee_minus_distributions, -10000000000); 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(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -886,7 +981,14 @@ fn update_pool_balances_revenue_to_fee_test() { // calling multiple times doesnt effect other than fee pool -> pnl pool 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(); + 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 @@ -897,7 +999,14 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market.revenue_pool.scaled_balance, 0); 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(); + 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 @@ -914,7 +1023,14 @@ fn update_pool_balances_revenue_to_fee_test() { let spot_market_backup = spot_market; let market_backup = market; 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 + 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; @@ -928,7 +1044,14 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market_vault_amount, 10100000001); 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(); + 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); @@ -943,7 +1066,14 @@ fn update_pool_balances_revenue_to_fee_test() { // calling again only does fee -> pnl pool 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(); + 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); @@ -957,7 +1087,14 @@ fn update_pool_balances_revenue_to_fee_test() { // calling again does nothing 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(); + 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); @@ -1007,9 +1144,14 @@ fn update_pool_balances_revenue_to_fee_test() { let market_backup = market; let spot_market_backup = spot_market; 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 + 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; @@ -1026,8 +1168,22 @@ fn update_pool_balances_revenue_to_fee_test() { ); 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!(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); @@ -1106,7 +1262,14 @@ fn update_pool_balances_revenue_to_fee_devnet_state_test() { let prev_tfmd = market.amm.total_fee_minus_distributions; 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(); + 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); @@ -1198,7 +1361,14 @@ fn update_pool_balances_revenue_to_fee_new_market() { // let prev_tfmd = market.amm.total_fee_minus_distributions; 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(); + 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 diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs index 6545ac65ae..5bf940354b 100644 --- a/programs/drift/src/controller/isolated_position.rs +++ b/programs/drift/src/controller/isolated_position.rs @@ -1,27 +1,23 @@ use std::cell::RefMut; -use anchor_lang::prelude::*; +use crate::controller; use crate::controller::spot_balance::update_spot_balances; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; use crate::error::{DriftResult, ErrorCode}; +use crate::get_then_update_id; use crate::math::casting::Cast; use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; -use crate::state::events::{ - DepositDirection, DepositExplanation, DepositRecord, -}; +use crate::state::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::oracle_map::OracleMap; -use crate::state::spot_market_map::SpotMarketMap; 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::state::user::{User, UserStats}; use crate::validate; -use crate::controller; -use crate::get_then_update_id; +use anchor_lang::prelude::*; #[cfg(test)] mod tests; @@ -417,4 +413,4 @@ pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( emit!(deposit_record); Ok(()) -} \ No newline at end of file +} diff --git a/programs/drift/src/controller/isolated_position/tests.rs b/programs/drift/src/controller/isolated_position/tests.rs index af29a122d9..2c2cac1660 100644 --- a/programs/drift/src/controller/isolated_position/tests.rs +++ b/programs/drift/src/controller/isolated_position/tests.rs @@ -8,11 +8,9 @@ pub mod deposit_into_isolated_perp_position { use solana_program::pubkey::Pubkey; use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, - LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, - QUOTE_PRECISION_U64, QUOTE_PRECISION_I128, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + 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; @@ -20,12 +18,10 @@ pub mod deposit_into_isolated_perp_position { 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::{create_anchor_account_info, test_utils::*}; + 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() { @@ -111,8 +107,14 @@ pub mod deposit_into_isolated_perp_position { ) .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.perp_positions[0].isolated_position_scaled_balance, + 1000000000 + ); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); } #[test] @@ -205,7 +207,6 @@ pub mod deposit_into_isolated_perp_position { assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); } - } pub mod transfer_isolated_perp_position_deposit { @@ -217,11 +218,8 @@ pub mod transfer_isolated_perp_position_deposit { 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, + 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; @@ -229,12 +227,13 @@ pub mod transfer_isolated_perp_position_deposit { 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::{create_anchor_account_info, test_utils::*, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + 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() { @@ -324,8 +323,14 @@ pub mod transfer_isolated_perp_position_deposit { ) .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.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); } @@ -480,7 +485,7 @@ pub mod transfer_isolated_perp_position_deposit { cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 6, initial_asset_weight: SPOT_WEIGHT_PRECISION, - deposit_balance: 2* SPOT_BALANCE_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, @@ -510,7 +515,7 @@ pub mod transfer_isolated_perp_position_deposit { now, 0, 0, - 2* QUOTE_PRECISION_I64, + 2 * QUOTE_PRECISION_I64, ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); @@ -607,9 +612,15 @@ pub mod transfer_isolated_perp_position_deposit { .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.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); - assert_eq!(user.spot_positions[0].scaled_balance, SPOT_BALANCE_PRECISION_U64); + assert_eq!( + user.spot_positions[0].scaled_balance, + SPOT_BALANCE_PRECISION_U64 + ); } #[test] @@ -806,11 +817,9 @@ pub mod withdraw_from_isolated_perp_position { use solana_program::pubkey::Pubkey; use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, - LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, - QUOTE_PRECISION_U64, QUOTE_PRECISION_I128, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + 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; @@ -818,12 +827,13 @@ pub mod withdraw_from_isolated_perp_position { 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::{create_anchor_account_info, test_utils::*, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + 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() { @@ -918,7 +928,10 @@ pub mod withdraw_from_isolated_perp_position { .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.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); } #[test] @@ -1108,5 +1121,4 @@ pub mod withdraw_from_isolated_perp_position { assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); } - -} \ No newline at end of file +} diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 9ffe22c0b2..aab1707765 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -31,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, PositionFlag, 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}; @@ -2454,7 +2455,8 @@ pub mod liquidate_perp { 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 perp_market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); let mut spot_market = SpotMarket { market_index: 0, @@ -2565,7 +2567,6 @@ pub mod liquidate_perp { assert_eq!(isolated_position_before, isolated_position_after); } - } pub mod liquidate_perp_with_fill { @@ -10237,7 +10238,8 @@ pub mod liquidate_isolated_perp { slot, now, &state, - ).unwrap(); + ) + .unwrap(); let spot_position_one_after = user.spot_positions[0].clone(); let spot_position_two_after = user.spot_positions[1].clone(); @@ -10256,6 +10258,7 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { 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; @@ -10276,12 +10279,11 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { 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::{PositionFlag, UserStats}; 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}; - use crate::controller::liquidation::resolve_perp_bankruptcy; - + #[test] pub fn successful_liquidation_liquidator_max_pnl_transfer() { let now = 0_i64; @@ -10422,7 +10424,10 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { ) .unwrap(); - assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 39494950000); + assert_eq!( + user.perp_positions[0].isolated_position_scaled_balance, + 39494950000 + ); assert_eq!(user.perp_positions[0].quote_asset_amount, -50000000); assert_eq!( @@ -10577,7 +10582,10 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { 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!( + 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); @@ -10613,13 +10621,18 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { 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.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 crate::state::liquidation_mode::{ + CrossMarginLiquidatePerpMode, IsolatedMarginLiquidatePerpMode, LiquidatePerpMode, + }; use std::collections::BTreeSet; use std::str::FromStr; @@ -10629,11 +10642,9 @@ mod liquidation_mode { 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, + 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; @@ -10646,9 +10657,9 @@ mod liquidation_mode { 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; - + use crate::test_utils::*; + #[test] pub fn tests_meets_margin_requirements() { let slot = 0_u64; @@ -10702,7 +10713,8 @@ mod liquidation_mode { 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 market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); let mut usdc_market = SpotMarket { market_index: 0, @@ -10781,16 +10793,28 @@ mod liquidation_mode { 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(); + 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); + 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 { @@ -10819,17 +10843,27 @@ mod liquidation_mode { ..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(); + 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); + assert_eq!( + cross_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + false + ); + assert_eq!( + isolated_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + true + ); } - } - \ No newline at end of file diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index b3dd569c3d..8473ccc6cf 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -237,9 +237,12 @@ pub fn settle_pnl( 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()? + user.perp_positions[position_index] + .get_isolated_token_amount(spot_market)? + .cast()? } else { - user.get_quote_spot_position().get_signed_token_amount(spot_market)? + user.get_quote_spot_position() + .get_signed_token_amount(spot_market)? }; let pnl_to_settle_with_user = update_pool_balances( diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index dcb3f02f3a..e2f10ac990 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -2221,7 +2221,8 @@ pub fn isolated_perp_position_negative_pnl() { 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; + 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; @@ -2354,7 +2355,8 @@ pub fn isolated_perp_position_user_unsettled_positive_pnl_less_than_pool() { 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; + 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; diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 71ab46c80c..a8158e5a73 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -1997,7 +1997,12 @@ fn isolated_perp_position() { .unwrap(); assert_eq!(perp_position.isolated_position_scaled_balance, 1000000000); - assert_eq!(perp_position.get_isolated_token_amount(&spot_market).unwrap(), amount); + assert_eq!( + perp_position + .get_isolated_token_amount(&spot_market) + .unwrap(), + amount + ); update_spot_balances( amount, @@ -2005,7 +2010,8 @@ fn isolated_perp_position() { &mut spot_market, &mut perp_position, false, - ).unwrap(); + ) + .unwrap(); assert_eq!(perp_position.isolated_position_scaled_balance, 0); @@ -2018,4 +2024,4 @@ fn isolated_perp_position() { ); assert_eq!(result, Err(ErrorCode::CantUpdateSpotBalanceType)); -} \ No newline at end of file +} diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index aa4cc5d331..65863c4a8a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -530,11 +530,12 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0, )?; - let perp_position_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { - market_position.max_margin_ratio as u32 - } else { - 0_u32 - }; + let perp_position_custom_margin_ratio = + if context.margin_type == MarginRequirementType::Initial { + market_position.max_margin_ratio as u32 + } else { + 0_u32 + }; let (perp_margin_requirement, weighted_pnl, worst_case_liability_value, base_asset_value) = calculate_perp_position_value_and_pnl( diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index a288a57be1..9f997e11f3 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4656,7 +4656,7 @@ mod get_margin_calculation_for_disable_high_leverage_mode { use crate::state::user::{Order, PerpPosition, SpotPosition, User}; use crate::test_utils::get_pyth_price; use crate::test_utils::*; - use crate::{create_account_info, MARGIN_PRECISION, create_anchor_account_info}; + use crate::{create_account_info, create_anchor_account_info, MARGIN_PRECISION}; #[test] pub fn check_user_not_changed() { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 69c2773ffd..885f1001f3 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1112,7 +1112,11 @@ impl PerpPosition { } pub fn is_available(&self) -> bool { - !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() && self.isolated_position_scaled_balance == 0 && !self.is_being_liquidated() + !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 { From 71fcdfa9dee7156b2105e7be3b9e3231362490be Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 20 Sep 2025 12:29:45 -0400 Subject: [PATCH 081/159] first ts test --- sdk/src/driftClient.ts | 197 +++++++++- sdk/src/idl/drift.json | 217 ++++++++++- sdk/src/types.ts | 2 + sdk/src/user.ts | 19 +- test-scripts/run-anchor-tests.sh | 1 + tests/isolatedPositionDriftClient.ts | 547 +++++++++++++++++++++++++++ 6 files changed, 966 insertions(+), 17 deletions(-) create mode 100644 tests/isolatedPositionDriftClient.ts diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 01dfa60030..abff8433df 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -2204,6 +2204,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. @@ -3772,6 +3781,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 @@ -9240,8 +9434,7 @@ export class DriftClient { public async updateUserGovTokenInsuranceStake( authority: PublicKey, - txParams?: TxParams, - env: DriftEnv = 'mainnet-beta' + txParams?: TxParams ): Promise { const ix = await this.getUpdateUserGovTokenInsuranceStakeIx(authority); const tx = await this.buildTransaction(ix, txParams); diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 3fab52decf..e621bcb8df 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": [ @@ -11486,13 +11643,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", @@ -11531,8 +11688,8 @@ "type": "u8" }, { - "name": "perLpBase", - "type": "i8" + "name": "positionFlag", + "type": "u8" } ] } @@ -12170,6 +12327,17 @@ ] } }, + { + "name": "LiquidationBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + } + ] + } + }, { "name": "SettlePnlExplanation", "type": { @@ -12282,13 +12450,7 @@ "kind": "enum", "variants": [ { - "name": "Standard", - "fields": [ - { - "name": "trackOpenOrdersFraction", - "type": "bool" - } - ] + "name": "Standard" }, { "name": "Liquidation", @@ -12850,6 +13012,23 @@ ] } }, + { + "name": "PositionFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + }, + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + } + ] + } + }, { "name": "ReferrerStatus", "type": { @@ -13797,6 +13976,11 @@ "defined": "SpotBankruptcyRecord" }, "index": false + }, + { + "name": "bitFlags", + "type": "u8", + "index": false } ] }, @@ -15105,8 +15289,8 @@ }, { "code": 6094, - "name": "CantUpdatePoolBalanceType", - "msg": "CantUpdatePoolBalanceType" + "name": "CantUpdateSpotBalanceType", + "msg": "CantUpdateSpotBalanceType" }, { "code": 6095, @@ -16217,6 +16401,11 @@ "code": 6316, "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" + }, + { + "code": 6317, + "name": "InvalidIsolatedPerpMarket", + "msg": "Invalid Isolated Perp Market" } ], "metadata": { diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 2d49e61b58..f263e2b9cc 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1110,6 +1110,8 @@ export type PerpPosition = { lastBaseAssetAmountPerLp: BN; lastQuoteAssetAmountPerLp: BN; perLpBase: number; + isolatedPositionScaledBalance: BN; + positionFlag: number; }; export type UserStatsAccount = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 08494f71f8..7b78495e21 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -334,6 +334,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; @@ -570,7 +586,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 e8ef72b4ea..a352caf9ef 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -36,6 +36,7 @@ test_files=( highLeverageMode.ts ifRebalance.ts insuranceFundStake.ts + isolatedPositionDriftClient.ts liquidateBorrowForPerpPnl.ts liquidatePerp.ts liquidatePerpWithFill.ts diff --git a/tests/isolatedPositionDriftClient.ts b/tests/isolatedPositionDriftClient.ts new file mode 100644 index 0000000000..644ae91308 --- /dev/null +++ b/tests/isolatedPositionDriftClient.ts @@ -0,0 +1,547 @@ +import * as anchor from '@coral-xyz/anchor'; +import { assert } from 'chai'; +import { BN, OracleSource, ZERO } from '../sdk'; + +import { Program } from '@coral-xyz/anchor'; + +import { PublicKey } from '@solana/web3.js'; + +import { TestClient, PositionDirection, EventSubscriber } from '../sdk/src'; + +import { + mockUSDCMint, + mockUserUSDCAccount, + mockOracleNoProgram, + setFeedPriceNoProgram, + initializeQuoteSpotMarket, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('drift client', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bankrunContextWrapper: BankrunContextWrapper; + + let bulkAccountLoader: TestBulkAccountLoader; + + let userAccountPublicKey: PublicKey; + + let usdcMint; + let userUSDCAccount; + + let solUsd; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(100000); + const ammInitialQuoteAssetAmount = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetAmount = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 1); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + userStats: true, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + + await driftClient.subscribe(); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + + const periodicity = new BN(60 * 60); // 1 HOUR + + await driftClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetAmount, + ammInitialQuoteAssetAmount, + periodicity + ); + + await driftClient.updatePerpMarketStepSizeAndTickSize( + 0, + new BN(1), + new BN(1) + ); + }); + + after(async () => { + await driftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('Initialize user account and deposit collateral', async () => { + await driftClient.initializeUserAccount(); + + userAccountPublicKey = await driftClient.getUserAccountPublicKey(); + + const txSig = await driftClient.depositIntoIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + const depositTokenAmount = + driftClient.getIsolatedPerpPositionTokenAmount(0); + console.log('depositTokenAmount', depositTokenAmount.toString()); + assert(depositTokenAmount.eq(usdcAmount)); + + // Check that drift collateral account has proper collateral + const quoteSpotVault = + await bankrunContextWrapper.connection.getTokenAccount( + driftClient.getQuoteSpotMarketAccount().vault + ); + + assert.ok(new BN(Number(quoteSpotVault.amount)).eq(usdcAmount)); + + await eventSubscriber.awaitTx(txSig); + const depositRecord = eventSubscriber.getEventsArray('DepositRecord')[0]; + + assert.ok( + depositRecord.userAuthority.equals( + bankrunContextWrapper.provider.wallet.publicKey + ) + ); + assert.ok(depositRecord.user.equals(userAccountPublicKey)); + + assert.ok( + JSON.stringify(depositRecord.direction) === + JSON.stringify({ deposit: {} }) + ); + assert.ok(depositRecord.amount.eq(new BN(10000000))); + }); + + it('Transfer isolated perp position deposit', async () => { + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount.neg(), 0); + + const quoteAssetTokenAmount = + driftClient.getIsolatedPerpPositionTokenAmount(0); + assert(quoteAssetTokenAmount.eq(ZERO)); + + const quoteTokenAmount = driftClient.getQuoteAssetTokenAmount(); + assert(quoteTokenAmount.eq(usdcAmount)); + + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount, 0); + + const quoteAssetTokenAmount2 = + driftClient.getIsolatedPerpPositionTokenAmount(0); + assert(quoteAssetTokenAmount2.eq(usdcAmount)); + + const quoteTokenAmoun2 = driftClient.getQuoteAssetTokenAmount(); + assert(quoteTokenAmoun2.eq(ZERO)); + }); + + it('Withdraw Collateral', async () => { + await driftClient.withdrawFromIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + await driftClient.fetchAccounts(); + assert(driftClient.getIsolatedPerpPositionTokenAmount(0).eq(ZERO)); + + // Check that drift collateral account has proper collateral] + const quoteSpotVault = + await bankrunContextWrapper.connection.getTokenAccount( + driftClient.getQuoteSpotMarketAccount().vault + ); + + assert.ok(new BN(Number(quoteSpotVault.amount)).eq(ZERO)); + + const userUSDCtoken = + await bankrunContextWrapper.connection.getTokenAccount( + userUSDCAccount.publicKey + ); + assert.ok(new BN(Number(userUSDCtoken.amount)).eq(usdcAmount)); + + const depositRecord = eventSubscriber.getEventsArray('DepositRecord')[0]; + + assert.ok( + depositRecord.userAuthority.equals( + bankrunContextWrapper.provider.wallet.publicKey + ) + ); + assert.ok(depositRecord.user.equals(userAccountPublicKey)); + + assert.ok( + JSON.stringify(depositRecord.direction) === + JSON.stringify({ withdraw: {} }) + ); + assert.ok(depositRecord.amount.eq(new BN(10000000))); + }); + + it('Long from 0 position', async () => { + // Re-Deposit USDC, assuming we have 0 balance here + await driftClient.depositIntoIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + const marketIndex = 0; + const baseAssetAmount = new BN(48000000000); + const txSig = await driftClient.openPosition( + PositionDirection.LONG, + baseAssetAmount, + marketIndex + ); + bankrunContextWrapper.connection.printTxLogs(txSig); + + const marketData = driftClient.getPerpMarketAccount(0); + await setFeedPriceNoProgram( + bankrunContextWrapper, + 1.01, + marketData.amm.oracle + ); + + const orderR = eventSubscriber.getEventsArray('OrderActionRecord')[0]; + console.log(orderR.takerFee.toString()); + console.log(orderR.baseAssetAmountFilled.toString()); + + const user: any = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + + console.log( + 'getQuoteAssetTokenAmount:', + driftClient.getIsolatedPerpPositionTokenAmount(0).toString() + ); + assert( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(10000000)) + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(48001)) + ); + + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(-48000001))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(-48048002))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(48000000000))); + assert.ok(user.perpPositions[0].positionFlag === 1); + + const market = driftClient.getPerpMarketAccount(0); + console.log(market.amm.baseAssetAmountWithAmm.toNumber()); + console.log(market); + + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(48000000000))); + console.log(market.amm.totalFee.toString()); + assert.ok(market.amm.totalFee.eq(new BN(48001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(48001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(1))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(48000001))); + assert.ok(orderActionRecord.marketIndex === marketIndex); + + assert.ok(orderActionRecord.takerExistingQuoteEntryAmount === null); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + + assert(driftClient.getPerpMarketAccount(0).nextFillRecordId.eq(new BN(2))); + }); + + it('Withdraw fails due to insufficient collateral', async () => { + // lil hack to stop printing errors + const oldConsoleLog = console.log; + const oldConsoleError = console.error; + console.log = function () { + const _noop = ''; + }; + console.error = function () { + const _noop = ''; + }; + try { + await driftClient.withdrawFromIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + assert(false, 'Withdrawal succeeded'); + } catch (e) { + assert(true); + } finally { + console.log = oldConsoleLog; + console.error = oldConsoleError; + } + }); + + it('Reduce long position', async () => { + const marketIndex = 0; + const baseAssetAmount = new BN(24000000000); + await driftClient.openPosition( + PositionDirection.SHORT, + baseAssetAmount, + marketIndex + ); + + await driftClient.fetchAccounts(); + + await driftClient.fetchAccounts(); + const user = driftClient.getUserAccount(); + console.log( + 'quoteAssetAmount:', + user.perpPositions[0].quoteAssetAmount.toNumber() + ); + console.log( + 'quoteBreakEvenAmount:', + user.perpPositions[0].quoteBreakEvenAmount.toNumber() + ); + + assert.ok(user.perpPositions[0].quoteAssetAmount.eq(new BN(-24072002))); + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(-24000001))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(-24048001))); + + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(24000000000))); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(10000000)) + ); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(72001)) + ); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(24000000000))); + assert.ok(market.amm.totalFee.eq(new BN(72001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(72001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(2))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(24000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(24000000))); + assert.ok(orderActionRecord.marketIndex === 0); + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000000)) + ); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + }); + + it('Reverse long position', async () => { + const marketData = driftClient.getPerpMarketAccount(0); + await setFeedPriceNoProgram( + bankrunContextWrapper, + 1.0, + marketData.amm.oracle + ); + + const baseAssetAmount = new BN(48000000000); + await driftClient.openPosition(PositionDirection.SHORT, baseAssetAmount, 0); + + await driftClient.fetchAccounts(); + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + await driftClient.fetchAccounts(); + const user = driftClient.getUserAccount(); + console.log( + 'quoteAssetAmount:', + user.perpPositions[0].quoteAssetAmount.toNumber() + ); + console.log( + 'quoteBreakEvenAmount:', + user.perpPositions[0].quoteBreakEvenAmount.toNumber() + ); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(9879998)) + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(120001)) + ); + console.log(user.perpPositions[0].quoteBreakEvenAmount.toString()); + console.log(user.perpPositions[0].quoteAssetAmount.toString()); + + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(24000000))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(23952000))); + assert.ok(user.perpPositions[0].quoteAssetAmount.eq(new BN(24000000))); + console.log(user.perpPositions[0].baseAssetAmount.toString()); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(-24000000000))); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(-24000000000))); + assert.ok(market.amm.totalFee.eq(new BN(120001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(120001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(3))); + console.log(orderActionRecord.baseAssetAmountFilled.toNumber()); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(48000000))); + + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000001)) + ); + assert.ok( + orderActionRecord.takerExistingBaseAssetAmount.eq(new BN(24000000000)) + ); + + assert.ok(orderActionRecord.marketIndex === 0); + }); + + it('Close position', async () => { + const marketIndex = 0; + await driftClient.closePosition(marketIndex); + + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + marketIndex + ); + + const user: any = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(0))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(0))); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(9855998)) + ); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(144001)) + ); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(0))); + assert.ok(market.amm.totalFee.eq(new BN(144001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(144001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(4))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(24000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(24000000))); + assert.ok(orderActionRecord.marketIndex === 0); + + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000000)) + ); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + }); + + it('Open short position', async () => { + const baseAssetAmount = new BN(48000000000); + await driftClient.openPosition(PositionDirection.SHORT, baseAssetAmount, 0); + + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + const user = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + assert.ok(user.perpPositions[0].positionFlag === 1); + console.log(user.perpPositions[0].quoteBreakEvenAmount.toString()); + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(47999999))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(47951999))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(-48000000000))); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(-48000000000))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(5))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(47999999))); + assert.ok(orderActionRecord.marketIndex === 0); + }); +}); From d1060ffb049d368d4d1614e70c204c7b09a6ea0b Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Sat, 20 Sep 2025 13:17:29 -0700 Subject: [PATCH 082/159] get max transfer bug fix --- programs/drift/src/state/lp_pool.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 6a23963cd4..6b455d7e8b 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -980,10 +980,10 @@ impl Constituent { let token_amount = self.get_full_token_amount(spot_market)?; let max_transfer = if token_amount < 0 { self.max_borrow_token_amount - .saturating_sub(token_amount.cast::()?) + .saturating_sub(token_amount.abs().cast::()?) } else { self.max_borrow_token_amount - .saturating_add(token_amount.cast::()?) + .saturating_add(token_amount.abs().cast::()?) }; Ok(max_transfer) From 7af9f657af6c7423ed392476b1cba4e198f02d34 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 21 Sep 2025 18:48:13 -0400 Subject: [PATCH 083/159] isolatedPositionLiquidatePerp test --- tests/isolatedPositionLiquidatePerp.ts | 425 +++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 tests/isolatedPositionLiquidatePerp.ts 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))); + }); +}); From e40563e3baabd19f0e339756480ec8eea5b33f1a Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 21 Sep 2025 18:54:26 -0400 Subject: [PATCH 084/159] isolatedPositionLiquidatePerpwithFill test --- .../isolatedPositionLiquidatePerpwithFill.ts | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 tests/isolatedPositionLiquidatePerpwithFill.ts 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 + ); + }); +}); From c643a50213f1ee64decd8462a92f860e365a9b1b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 23 Sep 2025 09:37:38 -0400 Subject: [PATCH 085/159] fix expired position --- programs/drift/src/controller/amm.rs | 40 +++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 93a190a4c7..ef920e7804 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -771,7 +771,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 { @@ -779,7 +779,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()?, @@ -810,14 +810,36 @@ 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) } From 16bea307238c3df88df9af00b00fbe9475b4eeee Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 23 Sep 2025 09:42:01 -0400 Subject: [PATCH 086/159] cargo fmt -- --- programs/drift/src/controller/amm.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index ef920e7804..f08b3b546d 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -813,7 +813,8 @@ pub fn update_pnl_pool_and_user_balance( 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)?; + let perp_position_token_amount = + perp_position.get_isolated_token_amount(quote_spot_market)?; if pnl_to_settle_with_user < 0 { validate!( From 010c210f7f3823c5fc777507cde05fb1b515a47b Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 25 Sep 2025 13:53:18 -0700 Subject: [PATCH 087/159] Wphan/merge-master (#1915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix comments (#1844) * chore: update laser 0.1.8 * chore: remove logging * program: tweak ResizeSignedMsgUserOrders (#1898) * fix linter and cargo test * fix cargo build errors * v2.138.0 * sdk: release v2.139.0-beta.0 * program: init-delegated-if-stake (#1859) * program: init-delegated-if-stake * add sdk * CHANGELOG --------- Co-authored-by: Chris Heaney * program: auction-order-params-on-slow-fast-twap-divergence (#1882) * program: auction-order-params-on-slow-fast-twap-divergence * change tests * rm dlog * CHANGELOG * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: add invariant for max in amount for if swap (#1825) * sdk: release v2.139.0-beta.1 * chore: add grpc client to order subscriber * sdk: release v2.139.0-beta.2 * sdk: add market index 76 to constant (#1901) * sdk: release v2.139.0-beta.3 * fix ui build (#1902) * sdk: release v2.139.0-beta.4 * sdk: update aster config (#1903) * update aster config * add pythLazerId * sdk: release v2.139.0-beta.5 * Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#1907) This reverts commit 0a8e15349f45e135df3eb2341f163d70ef09fe64. * sdk: release v2.139.0-beta.6 * Revert "Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#…" (#1910) * sdk: release v2.139.0-beta.7 * more robust isDelegateSigner for swift orders * sdk: release v2.139.0-beta.8 * program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit (#1909) * program: update-resolve-perp-pnl-pool-validate * CHANGELOG --------- Co-authored-by: Chris Heaney * program: add immutable owner support for token 22 vaults (#1904) * program: add immutable owner support for token 22 vaults * cargo fmt -- * CHANGELOG * sdk: tweak math for filling triggers (#1880) * sdk: tweak math for filling triggers * add back line * sdk: release v2.139.0-beta.9 * program: allow delegate to update user position max margin ratio (#1913) * Revert "more robust isDelegateSigner for swift orders" This reverts commit 2d4e30b5bfac835c2251b8640b898408714a7c13. * sdk: release v2.139.0-beta.10 * update SwiftOrderMessage type for missing fields (#1908) * sdk: release v2.139.0-beta.11 * sdk: add getUpdateFeatureBitFlagsMedianTriggerPriceIx * sdk: release v2.139.0-beta.12 * update devnet market constants (#1914) * sdk: release v2.139.0-beta.13 * program: deposit into if stake from admin (#1899) * program: deposit into if stake from admin * add test * change action * cargo fmt -- * move depositIntoInsuranceFundStake to adminClient --------- Co-authored-by: wphan * sdk: release v2.139.0-beta.14 * program: comment out unused ix (#1911) * program: raise MAX_BASE_ASSET_AMOUNT_WITH_AMM numerical invariant * v2.139.0 * sdk: release v2.140.0-beta.0 --------- Co-authored-by: jordy25519 Co-authored-by: Jack Waller Co-authored-by: lil perp Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> --- CHANGELOG.md | 22 ++ Cargo.lock | 2 +- package.json | 2 +- programs/drift/Cargo.toml | 2 +- programs/drift/src/controller/amm.rs | 4 +- programs/drift/src/controller/insurance.rs | 30 +- .../drift/src/controller/insurance/tests.rs | 23 +- programs/drift/src/controller/token.rs | 13 + programs/drift/src/instructions/admin.rs | 81 ++++- .../drift/src/instructions/constraints.rs | 10 +- programs/drift/src/instructions/if_staker.rs | 157 +++++++++- programs/drift/src/instructions/user.rs | 8 +- programs/drift/src/lib.rs | 90 +++--- programs/drift/src/math/constants.rs | 2 +- programs/drift/src/state/events.rs | 1 + programs/drift/src/state/order_params.rs | 68 ++-- .../drift/src/state/order_params/tests.rs | 69 +++-- programs/drift/src/state/perp_market.rs | 4 +- sdk/VERSION | 2 +- sdk/bun.lock | 21 ++ sdk/package.json | 3 +- .../accounts/laserProgramAccountSubscriber.ts | 215 +++++++++++++ sdk/src/accounts/types.ts | 1 + sdk/src/adminClient.ts | 132 +++++++- sdk/src/constants/perpMarkets.ts | 13 + sdk/src/constants/spotMarkets.ts | 12 + sdk/src/driftClient.ts | 2 +- sdk/src/idl/drift.json | 291 ++++++------------ sdk/src/isomorphic/grpc.node.ts | 17 +- sdk/src/math/auction.ts | 4 + sdk/src/math/orders.ts | 2 +- sdk/src/orderSubscriber/grpcSubscription.ts | 42 ++- sdk/src/swift/swiftOrderSubscriber.ts | 59 +++- sdk/yarn.lock | 70 +++++ tests/insuranceFundStake.ts | 29 ++ yarn.lock | 109 +++++-- 36 files changed, 1243 insertions(+), 369 deletions(-) create mode 100644 sdk/src/accounts/laserProgramAccountSubscriber.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3387fb8642..f206720572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +### Fixes + +### Breaking + +## [2.139.0] - 2025-09-25 + +### Features + +- program: all token 22 use immutable owner ([#1904](https://github.com/drift-labs/protocol-v2/pull/1904)) +- program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit ([#1909](https://github.com/drift-labs/protocol-v2/pull/1909)) +- program: auction order params account for twap divergence ([#1882](https://github.com/drift-labs/protocol-v2/pull/1882)) +- program: add delegate stake if ([#1859](https://github.com/drift-labs/protocol-v2/pull/1859)) + +### Fixes + +### Breaking + +## [2.138.0] - 2025-09-22 + +### Features + - program: support scaled ui extension ([#1894](https://github.com/drift-labs/protocol-v2/pull/1894)) +- Revert "Crispeaney/revert swift max margin ratio ([#1877](https://github.com/drift-labs/protocol-v2/pull/1877)) ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 58aef0bce6..cc1c58e9fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.137.0" +version = "2.139.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/package.json b/package.json index de9fdd10f5..854fea3020 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "prettify:fix": "prettier --write './sdk/src/**/*.ts' './tests/**.ts' './cli/**.ts'", "lint": "eslint . --ext ts --quiet --format unix", "lint:fix": "eslint . --ext ts --fix", - "update-idl": "cp target/idl/drift.json sdk/src/idl/drift.json" + "update-idl": "anchor build -- --features anchor-test && cp target/idl/drift.json sdk/src/idl/drift.json" }, "engines": { "node": ">=12" diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 87d0bb85e4..6f37757604 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.137.0" +version = "2.139.0" description = "Created with Anchor" edition = "2018" diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 1088b680e7..0012b4804f 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -11,7 +11,7 @@ use crate::controller::spot_balance::{ }; use crate::error::{DriftResult, ErrorCode}; use crate::get_then_update_id; -use crate::math::amm::calculate_quote_asset_amount_swapped; +use crate::math::amm::{calculate_net_user_pnl, calculate_quote_asset_amount_swapped}; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ @@ -942,7 +942,7 @@ pub fn calculate_perp_market_amm_summary_stats( .safe_add(fee_pool_token_amount)? .cast()?; - let net_user_pnl = amm::calculate_net_user_pnl(&perp_market.amm, perp_market_oracle_price)?; + let net_user_pnl = calculate_net_user_pnl(&perp_market.amm, perp_market_oracle_price)?; // amm's mm_fee can be incorrect with drifting integer math error let mut new_total_fee_minus_distributions = pnl_tokens_available.safe_sub(net_user_pnl)?; diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 91ad374ed5..17cb020405 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -112,6 +112,7 @@ pub fn add_insurance_fund_stake( user_stats: &mut UserStats, spot_market: &mut SpotMarket, now: i64, + admin_deposit: bool, ) -> DriftResult { validate!( !(insurance_vault_amount == 0 && spot_market.insurance_fund.total_shares != 0), @@ -161,7 +162,11 @@ pub fn add_insurance_fund_stake( emit!(InsuranceFundStakeRecord { ts: now, user_authority: user_stats.authority, - action: StakeAction::Stake, + action: if admin_deposit { + StakeAction::AdminDeposit + } else { + StakeAction::Stake + }, amount, market_index: spot_market.market_index, insurance_vault_amount_before: insurance_vault_amount, @@ -843,11 +848,20 @@ pub fn resolve_perp_pnl_deficit( &SpotBalanceType::Deposit, )?; + let net_user_pnl = calculate_net_user_pnl( + &market.amm, + market + .amm + .historical_oracle_data + .last_oracle_price_twap_5min, + )?; + validate!( - pnl_pool_token_amount == 0, + pnl_pool_token_amount.cast::()? < net_user_pnl, ErrorCode::SufficientPerpPnlPool, - "pnl_pool_token_amount > 0 (={})", - pnl_pool_token_amount + "pnl_pool_token_amount >= net_user_pnl ({} >= {})", + pnl_pool_token_amount, + net_user_pnl )?; update_spot_market_cumulative_interest(spot_market, None, now)?; @@ -1098,6 +1112,14 @@ pub fn handle_if_end_swap( if_rebalance_config.epoch_max_in_amount )?; + validate!( + if_rebalance_config.current_in_amount <= if_rebalance_config.total_in_amount, + ErrorCode::InvalidIfRebalanceSwap, + "current_in_amount={} > total_in_amount={}", + if_rebalance_config.current_in_amount, + if_rebalance_config.total_in_amount + )?; + let oracle_twap = out_spot_market .historical_oracle_data .last_oracle_price_twap; diff --git a/programs/drift/src/controller/insurance/tests.rs b/programs/drift/src/controller/insurance/tests.rs index 4301a3d26f..c71948c3d3 100644 --- a/programs/drift/src/controller/insurance/tests.rs +++ b/programs/drift/src/controller/insurance/tests.rs @@ -41,6 +41,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -104,6 +105,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -141,6 +143,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -202,6 +205,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -245,6 +249,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -334,6 +339,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 20, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 199033744205760); @@ -346,6 +352,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 30, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 398067488411520); @@ -378,6 +385,7 @@ pub fn gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -502,6 +510,7 @@ pub fn losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -631,6 +640,7 @@ pub fn escrow_losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -729,7 +739,8 @@ pub fn escrow_gains_stake_if_test() { &mut if_stake, &mut user_stats, &mut spot_market, - 0 + 0, + false, ) .is_err()); @@ -741,6 +752,7 @@ pub fn escrow_gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -858,6 +870,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .is_err()); @@ -877,6 +890,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += amount; @@ -912,6 +926,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut orig_user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1010,6 +1025,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1210,6 +1226,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all_2() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += 10_000_000_000_000; @@ -1254,6 +1271,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1266,6 +1284,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); @@ -1392,6 +1411,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1404,6 +1424,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/token.rs b/programs/drift/src/controller/token.rs index 8fde1d3b57..a36a2e8404 100644 --- a/programs/drift/src/controller/token.rs +++ b/programs/drift/src/controller/token.rs @@ -291,3 +291,16 @@ pub fn initialize_token_account<'info>( Ok(()) } + +pub fn initialize_immutable_owner<'info>( + token_program: &Interface<'info, TokenInterface>, + account: &AccountInfo<'info>, +) -> Result<()> { + let accounts = ::anchor_spl::token_interface::InitializeImmutableOwner { + account: account.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(token_program.to_account_info(), accounts); + ::anchor_spl::token_interface::initialize_immutable_owner(cpi_ctx)?; + + Ok(()) +} diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 6459c800cc..374ab4a407 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -9,7 +9,7 @@ use serum_dex::state::ToAlignedBytes; use std::convert::{identity, TryInto}; use std::mem::size_of; -use crate::controller::token::{close_vault, initialize_token_account}; +use crate::controller::token::{close_vault, initialize_immutable_owner, initialize_token_account}; use crate::error::ErrorCode; use crate::ids::{admin_hot_wallet, amm_spread_adjust_wallet, mm_oracle_crank_wallet}; use crate::instructions::constraints::*; @@ -17,11 +17,12 @@ use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::{ AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, DEFAULT_LIQUIDATION_MARGIN_BUFFER_RATIO, - FEE_POOL_TO_REVENUE_POOL_THRESHOLD, IF_FACTOR_PRECISION, INSURANCE_A_MAX, INSURANCE_B_MAX, - INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, LIQUIDATION_FEE_PRECISION, - MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, - PERCENTAGE_PRECISION_I64, QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, - SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, + FEE_POOL_TO_REVENUE_POOL_THRESHOLD, GOV_SPOT_MARKET_INDEX, IF_FACTOR_PRECISION, + INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, + LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, + MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, + QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, + SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, }; use crate::math::cp_curve::get_update_k_result; use crate::math::helpers::get_proportion_u128; @@ -45,6 +46,7 @@ use crate::state::fulfillment_params::serum::SerumContext; use crate::state::fulfillment_params::serum::SerumV3FulfillmentConfig; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::if_rebalance_config::{IfRebalanceConfig, IfRebalanceConfigParams}; +use crate::state::insurance_fund_stake::InsuranceFundStake; use crate::state::insurance_fund_stake::ProtocolIfSharesTransferConfig; use crate::state::oracle::get_sb_on_demand_price; use crate::state::oracle::{ @@ -150,6 +152,16 @@ pub fn handle_initialize_spot_market( let state = &mut ctx.accounts.state; let spot_market_pubkey = ctx.accounts.spot_market.key(); + let is_token_2022 = *ctx.accounts.spot_market_mint.to_account_info().owner == Token2022::id(); + if is_token_2022 { + initialize_immutable_owner(&ctx.accounts.token_program, &ctx.accounts.spot_market_vault)?; + + initialize_immutable_owner( + &ctx.accounts.token_program, + &ctx.accounts.insurance_fund_vault, + )?; + } + initialize_token_account( &ctx.accounts.token_program, &ctx.accounts.spot_market_vault, @@ -5000,6 +5012,39 @@ pub fn handle_update_feature_bit_flags_median_trigger_price( Ok(()) } +pub fn handle_update_delegate_user_gov_token_insurance_stake( + ctx: Context, +) -> Result<()> { + let insurance_fund_stake = &mut load_mut!(ctx.accounts.insurance_fund_stake)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + + validate!( + insurance_fund_stake.market_index == GOV_SPOT_MARKET_INDEX, + ErrorCode::IncorrectSpotMarketAccountPassed, + "insurance_fund_stake is not for governance market index = {}", + GOV_SPOT_MARKET_INDEX + )?; + + if insurance_fund_stake.market_index == GOV_SPOT_MARKET_INDEX + && spot_market.market_index == GOV_SPOT_MARKET_INDEX + { + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + crate::controller::insurance::update_user_stats_if_stake_amount( + 0, + ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + now, + )?; + } + + Ok(()) +} + pub fn handle_update_feature_bit_flags_settle_lp_pool( ctx: Context, enable: bool, @@ -5965,3 +6010,27 @@ pub struct UpdateIfRebalanceConfig<'info> { )] pub state: Box>, } + +#[derive(Accounts)] +pub struct UpdateDelegateUserGovTokenInsuranceStake<'info> { + #[account( + mut, + seeds = [b"spot_market", 15_u16.to_le_bytes().as_ref()], + bump + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + pub insurance_fund_stake: AccountLoader<'info, InsuranceFundStake>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), 15_u16.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + #[account( + has_one = admin + )] + pub state: Box>, +} diff --git a/programs/drift/src/instructions/constraints.rs b/programs/drift/src/instructions/constraints.rs index c983ea471f..23deccd715 100644 --- a/programs/drift/src/instructions/constraints.rs +++ b/programs/drift/src/instructions/constraints.rs @@ -159,15 +159,13 @@ pub fn get_vault_len(mint: &InterfaceAccount) -> anchor_lang::Result::unpack(&mint_data)?; let mint_extensions = match mint_state.get_extension_types() { Ok(extensions) => extensions, - // If we cant deserialize the mint, we use the default token account length + // If we cant deserialize the mint, try assuming no extensions // Init token will fail if this size doesnt work, so worst case init account just fails - Err(_) => { - msg!("Failed to deserialize mint. Falling back to default token account length"); - return Ok(::anchor_spl::token::TokenAccount::LEN); - } + Err(_) => vec![], }; - let required_extensions = + let mut required_extensions = ExtensionType::get_required_init_account_extensions(&mint_extensions); + required_extensions.push(ExtensionType::ImmutableOwner); ExtensionType::try_calculate_account_len::(&required_extensions)? } else { ::anchor_spl::token::TokenAccount::LEN diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 51e4d7fd79..1d15b5ff6f 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -3,7 +3,7 @@ use anchor_lang::Discriminator; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use crate::error::ErrorCode; -use crate::ids::if_rebalance_wallet; +use crate::ids::{admin_hot_wallet, if_rebalance_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::optional_accounts::get_token_mint; @@ -143,6 +143,7 @@ pub fn handle_add_insurance_fund_stake<'c: 'info, 'info>( user_stats, spot_market, clock.unix_timestamp, + false, )?; controller::token::receive( @@ -821,6 +822,114 @@ pub fn handle_transfer_protocol_if_shares_to_revenue_pool<'c: 'info, 'info>( Ok(()) } +pub fn handle_deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, +) -> Result<()> { + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let insurance_fund_stake = &mut load_mut!(ctx.accounts.insurance_fund_stake)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + let state = &ctx.accounts.state; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + + validate!( + !spot_market.is_insurance_fund_operation_paused(InsuranceFundOperation::Add), + ErrorCode::InsuranceFundOperationPaused, + "if staking add disabled", + )?; + + validate!( + insurance_fund_stake.market_index == market_index, + ErrorCode::IncorrectSpotMarketAccountPassed, + "insurance_fund_stake does not match market_index" + )?; + + validate!( + spot_market.status != MarketStatus::Initialized, + ErrorCode::InvalidSpotMarketState, + "spot market = {} not active for insurance_fund_stake", + spot_market.market_index + )?; + + validate!( + insurance_fund_stake.last_withdraw_request_shares == 0 + && insurance_fund_stake.last_withdraw_request_value == 0, + ErrorCode::IFWithdrawRequestInProgress, + "withdraw request in progress" + )?; + + { + if spot_market.has_transfer_hook() { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + Some(&mut remaining_accounts_iter.clone()), + )?; + } else { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + None, + )?; + }; + + // reload the vault balances so they're up-to-date + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.insurance_fund_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + } + + controller::insurance::add_insurance_fund_stake( + amount, + ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + clock.unix_timestamp, + true, + )?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.user_token_account, + &ctx.accounts.insurance_fund_vault, + &ctx.accounts.signer.to_account_info(), + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + Ok(()) +} + #[derive(Accounts)] #[instruction( market_index: u16, @@ -1082,3 +1191,49 @@ pub struct TransferProtocolIfSharesToRevenuePool<'info> { /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, } + +#[derive(Accounts)] +#[instruction(market_index: u16,)] +pub struct DepositIntoInsuranceFundStake<'info> { + pub signer: Signer<'info>, + #[account( + mut, + constraint = signer.key() == admin_hot_wallet::id() || signer.key() == state.admin + )] + pub state: Box>, + #[account( + mut, + seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], + bump + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + seeds = [b"insurance_fund_stake", user_stats.load()?.authority.as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_stake: AccountLoader<'info, InsuranceFundStake>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + #[account( + mut, + token::mint = insurance_fund_vault.mint, + token::authority = signer + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 2b4a90e19f..6bff0e6fb4 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -4008,6 +4008,9 @@ pub struct ResizeSignedMsgUserOrders<'info> { pub signed_msg_user_orders: Box>, /// CHECK: authority pub authority: AccountInfo<'info>, + #[account( + has_one = authority + )] pub user: AccountLoader<'info, User>, #[account(mut)] pub payer: Signer<'info>, @@ -4445,14 +4448,9 @@ pub struct UpdateUser<'info> { } #[derive(Accounts)] -#[instruction( - sub_account_id: u16, -)] pub struct UpdateUserPerpPositionCustomMarginRatio<'info> { #[account( mut, - seeds = [b"user", authority.key.as_ref(), sub_account_id.to_le_bytes().as_ref()], - bump, constraint = can_sign_for_user(&user, &authority)? )] pub user: AccountLoader<'info, User>, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index d9c6e3695e..0130e91ec7 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -413,13 +413,13 @@ pub mod drift { handle_update_user_reduce_only(ctx, _sub_account_id, reduce_only) } - pub fn update_user_advanced_lp( - ctx: Context, - _sub_account_id: u16, - advanced_lp: bool, - ) -> Result<()> { - handle_update_user_advanced_lp(ctx, _sub_account_id, advanced_lp) - } + // pub fn update_user_advanced_lp( + // ctx: Context, + // _sub_account_id: u16, + // advanced_lp: bool, + // ) -> Result<()> { + // handle_update_user_advanced_lp(ctx, _sub_account_id, advanced_lp) + // } pub fn update_user_protected_maker_orders( ctx: Context, @@ -525,9 +525,9 @@ pub mod drift { handle_update_user_stats_referrer_info(ctx) } - pub fn update_user_open_orders_count(ctx: Context) -> Result<()> { - handle_update_user_open_orders_count(ctx) - } + // pub fn update_user_open_orders_count(ctx: Context) -> Result<()> { + // handle_update_user_open_orders_count(ctx) + // } pub fn admin_disable_update_perp_bid_ask_twap( ctx: Context, @@ -725,6 +725,7 @@ pub mod drift { handle_update_spot_market_expiry(ctx, expiry_ts) } + // IF stakers pub fn update_user_quote_asset_insurance_stake( ctx: Context, ) -> Result<()> { @@ -737,7 +738,11 @@ pub mod drift { handle_update_user_gov_token_insurance_stake(ctx) } - // IF stakers + pub fn update_delegate_user_gov_token_insurance_stake( + ctx: Context, + ) -> Result<()> { + handle_update_delegate_user_gov_token_insurance_stake(ctx) + } pub fn initialize_insurance_fund_stake( ctx: Context, @@ -776,13 +781,14 @@ pub mod drift { handle_remove_insurance_fund_stake(ctx, market_index) } - pub fn transfer_protocol_if_shares( - ctx: Context, - market_index: u16, - shares: u128, - ) -> Result<()> { - handle_transfer_protocol_if_shares(ctx, market_index, shares) - } + // pub fn transfer_protocol_if_shares( + // ctx: Context, + // market_index: u16, + // shares: u128, + // ) -> Result<()> { + // handle_transfer_protocol_if_shares(ctx, market_index, shares) + // } + pub fn begin_insurance_fund_swap<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InsuranceFundSwap<'info>>, in_market_index: u16, @@ -808,6 +814,14 @@ pub mod drift { handle_transfer_protocol_if_shares_to_revenue_pool(ctx, market_index, amount) } + pub fn deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, + ) -> Result<()> { + handle_deposit_into_insurance_fund_stake(ctx, market_index, amount) + } + pub fn update_pyth_pull_oracle( ctx: Context, feed_id: [u8; 32], @@ -937,9 +951,9 @@ pub mod drift { handle_update_phoenix_fulfillment_config_status(ctx, status) } - pub fn update_serum_vault(ctx: Context) -> Result<()> { - handle_update_serum_vault(ctx) - } + // pub fn update_serum_vault(ctx: Context) -> Result<()> { + // handle_update_serum_vault(ctx) + // } pub fn initialize_perp_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InitializePerpMarket<'info>>, @@ -1700,23 +1714,23 @@ pub mod drift { handle_update_spot_auction_duration(ctx, default_spot_auction_duration) } - pub fn initialize_protocol_if_shares_transfer_config( - ctx: Context, - ) -> Result<()> { - handle_initialize_protocol_if_shares_transfer_config(ctx) - } - - pub fn update_protocol_if_shares_transfer_config( - ctx: Context, - whitelisted_signers: Option<[Pubkey; 4]>, - max_transfer_per_epoch: Option, - ) -> Result<()> { - handle_update_protocol_if_shares_transfer_config( - ctx, - whitelisted_signers, - max_transfer_per_epoch, - ) - } + // pub fn initialize_protocol_if_shares_transfer_config( + // ctx: Context, + // ) -> Result<()> { + // handle_initialize_protocol_if_shares_transfer_config(ctx) + // } + + // pub fn update_protocol_if_shares_transfer_config( + // ctx: Context, + // whitelisted_signers: Option<[Pubkey; 4]>, + // max_transfer_per_epoch: Option, + // ) -> Result<()> { + // handle_update_protocol_if_shares_transfer_config( + // ctx, + // whitelisted_signers, + // max_transfer_per_epoch, + // ) + // } pub fn initialize_prelaunch_oracle( ctx: Context, diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index e5bf55798d..cf9148aff6 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -170,7 +170,7 @@ pub const MAX_K_BPS_INCREASE: i128 = TEN_BPS; pub const MAX_K_BPS_DECREASE: i128 = TWO_PT_TWO_PCT; pub const MAX_UPDATE_K_PRICE_CHANGE: u128 = HUNDRENTH_OF_CENT; pub const MAX_SQRT_K: u128 = 1000000000000000000000; // 1e21 (count 'em!) -pub const MAX_BASE_ASSET_AMOUNT_WITH_AMM: u128 = 100000000000000000; // 1e17 (count 'em!) +pub const MAX_BASE_ASSET_AMOUNT_WITH_AMM: u128 = 400000000000000000; // 4e17 (count 'em!) pub const MAX_PEG_BPS_INCREASE: u128 = TEN_BPS as u128; // 10 bps increase pub const MAX_PEG_BPS_DECREASE: u128 = TEN_BPS as u128; // 10 bps decrease diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 55e9cecaeb..2d6c20fa7a 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -580,6 +580,7 @@ pub enum StakeAction { Unstake, UnstakeTransfer, StakeTransfer, + AdminDeposit, } #[event] diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 69e1c6710d..3b3431a38c 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -702,13 +702,6 @@ impl OrderParams { } .cast::()?; - let baseline_start_price_offset_slow = mark_twap_slow.safe_sub( - perp_market - .amm - .historical_oracle_data - .last_oracle_price_twap, - )?; - let baseline_start_price_offset_fast = perp_market .amm .last_mark_price_twap_5min @@ -720,27 +713,42 @@ impl OrderParams { .last_oracle_price_twap_5min, )?; - let frac_of_long_spread_in_price: i64 = perp_market - .amm - .long_spread - .cast::()? - .safe_mul(mark_twap_slow)? - .safe_div(PRICE_PRECISION_I64 * 10)?; + let baseline_start_price_offset_slow = mark_twap_slow.safe_sub( + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + )?; - let frac_of_short_spread_in_price: i64 = perp_market - .amm - .short_spread - .cast::()? - .safe_mul(mark_twap_slow)? - .safe_div(PRICE_PRECISION_I64 * 10)?; - - let baseline_start_price_offset = match direction { - PositionDirection::Long => baseline_start_price_offset_slow - .safe_add(frac_of_long_spread_in_price)? - .min(baseline_start_price_offset_fast.safe_sub(frac_of_short_spread_in_price)?), - PositionDirection::Short => baseline_start_price_offset_slow - .safe_sub(frac_of_short_spread_in_price)? - .max(baseline_start_price_offset_fast.safe_add(frac_of_long_spread_in_price)?), + let baseline_start_price_offset = if baseline_start_price_offset_slow + .abs_diff(baseline_start_price_offset_fast) + <= perp_market.amm.last_mark_price_twap_5min / 200 + { + let frac_of_long_spread_in_price: i64 = perp_market + .amm + .long_spread + .cast::()? + .safe_mul(mark_twap_slow)? + .safe_div(PRICE_PRECISION_I64 * 10)?; + + let frac_of_short_spread_in_price: i64 = perp_market + .amm + .short_spread + .cast::()? + .safe_mul(mark_twap_slow)? + .safe_div(PRICE_PRECISION_I64 * 10)?; + + match direction { + PositionDirection::Long => baseline_start_price_offset_slow + .safe_add(frac_of_long_spread_in_price)? + .min(baseline_start_price_offset_fast.safe_sub(frac_of_short_spread_in_price)?), + PositionDirection::Short => baseline_start_price_offset_slow + .safe_sub(frac_of_short_spread_in_price)? + .max(baseline_start_price_offset_fast.safe_add(frac_of_long_spread_in_price)?), + } + } else { + // more than 50bps different of fast/slow twap, use fast only + baseline_start_price_offset_fast }; Ok(baseline_start_price_offset) @@ -891,15 +899,15 @@ fn get_auction_duration( ) -> DriftResult { let percent_diff = price_diff.safe_mul(PERCENTAGE_PRECISION_U64)?.div(price); - let slots_per_bp = if contract_tier.is_as_safe_as_contract(&ContractTier::B) { + let slots_per_pct = if contract_tier.is_as_safe_as_contract(&ContractTier::B) { 100 } else { 60 }; Ok(percent_diff - .safe_mul(slots_per_bp)? - .safe_div_ceil(PERCENTAGE_PRECISION_U64 / 100)? // 1% = 60 slots + .safe_mul(slots_per_pct)? + .safe_div_ceil(PERCENTAGE_PRECISION_U64 / 100)? // 1% = 40 slots .clamp(1, 180) as u8) // 180 slots max } diff --git a/programs/drift/src/state/order_params/tests.rs b/programs/drift/src/state/order_params/tests.rs index 5f59a6c3f3..d2d54af789 100644 --- a/programs/drift/src/state/order_params/tests.rs +++ b/programs/drift/src/state/order_params/tests.rs @@ -409,10 +409,11 @@ mod update_perp_auction_params { ..AMM::default() }; amm.last_bid_price_twap = (oracle_price * 15 / 10 - 192988) as u64; - amm.last_mark_price_twap_5min = (oracle_price * 16 / 10) as u64; + amm.last_mark_price_twap_5min = (oracle_price * 155 / 100) as u64; amm.last_ask_price_twap = (oracle_price * 16 / 10 + 192988) as u64; amm.historical_oracle_data.last_oracle_price_twap = oracle_price; amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; amm.historical_oracle_data.last_oracle_price = oracle_price; let perp_market = PerpMarket { @@ -436,7 +437,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); - assert_eq!(order_params_after.auction_start_price, Some(72_524_319)); + assert_eq!(order_params_after.auction_start_price, Some(79_750_000)); assert_eq!(order_params_after.auction_end_price, Some(90_092_988)); assert_eq!(order_params_after.auction_duration, Some(180)); } @@ -456,13 +457,13 @@ mod update_perp_auction_params { ..AMM::default() }; amm.last_bid_price_twap = (oracle_price * 99 / 100) as u64; - amm.last_mark_price_twap_5min = oracle_price as u64; amm.last_ask_price_twap = (oracle_price * 101 / 100) as u64; amm.historical_oracle_data.last_oracle_price_twap = oracle_price; amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; amm.historical_oracle_data.last_oracle_price = oracle_price; - let perp_market = PerpMarket { + let mut perp_market = PerpMarket { amm, ..PerpMarket::default() }; @@ -563,10 +564,10 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_duration, Some(175)); + assert_eq!(order_params_after.auction_duration, Some(120)); assert_eq!( order_params_after.auction_start_price, - Some(100 * PRICE_PRECISION_I64 - 901000) + Some(100 * PRICE_PRECISION_I64) ); assert_eq!( order_params_after.auction_end_price, @@ -599,20 +600,24 @@ mod update_perp_auction_params { direction: PositionDirection::Short, ..OrderParams::default() }; + + // tighten bid/ask to mark twap 5min to activate buffer + perp_market.amm.last_bid_price_twap = amm.last_mark_price_twap_5min - 100000; + perp_market.amm.last_ask_price_twap = amm.last_mark_price_twap_5min + 100000; let mut order_params_after = order_params_before; order_params_after .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_duration, Some(174)); assert_eq!( order_params_after.auction_start_price, - Some(100 * PRICE_PRECISION_I64 + 899000) // %1 / 10 = 10 bps aggression + Some(100 * PRICE_PRECISION_I64 + 100100) // a bit more passive than mid ); assert_eq!( order_params_after.auction_end_price, Some(98 * PRICE_PRECISION_I64) ); + assert_eq!(order_params_after.auction_duration, Some(127)); } #[test] @@ -786,10 +791,12 @@ mod update_perp_auction_params { }; amm.historical_oracle_data.last_oracle_price = oracle_price; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let mut perp_market = PerpMarket { amm, @@ -812,7 +819,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 98901080); + assert_eq!(order_params_after.auction_start_price.unwrap(), 99018698); let amm_bid_price = amm.bid_price(amm.reserve_price().unwrap()).unwrap(); assert_eq!(amm_bid_price, 98010000); assert!(order_params_after.auction_start_price.unwrap() as u64 > amm_bid_price); @@ -832,7 +839,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 99118879); + assert_eq!(order_params_after.auction_start_price.unwrap(), 99216738); // skip for prelaunch oracle perp_market.amm.oracle_source = OracleSource::Prelaunch; @@ -893,12 +900,13 @@ mod update_perp_auction_params { ..AMM::default() }; amm.historical_oracle_data.last_oracle_price = oracle_price; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; - + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let perp_market = PerpMarket { amm, contract_tier: ContractTier::B, @@ -920,7 +928,8 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), -98920); + assert_eq!(order_params_after.auction_start_price.unwrap(), 18698); + assert_eq!(order_params_after.auction_end_price.unwrap(), 2196053); let order_params_before = OrderParams { order_type: OrderType::Oracle, @@ -985,10 +994,12 @@ mod update_perp_auction_params { }; amm.historical_oracle_data.last_oracle_price = oracle_price; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let perp_market = PerpMarket { amm, @@ -1011,7 +1022,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 98653580); + assert_eq!(order_params_after.auction_start_price.unwrap(), 98771198); let order_params_before = OrderParams { order_type: OrderType::Market, @@ -1030,7 +1041,7 @@ mod update_perp_auction_params { assert_ne!(order_params_before, order_params_after); assert_eq!( order_params_after.auction_start_price.unwrap(), - (99 * PRICE_PRECISION_I64 - oracle_price / 400) - 98920 // approx equal with some noise + (99 * PRICE_PRECISION_I64 - oracle_price / 400 + 18698) // approx equal with some noise ); let order_params_before = OrderParams { @@ -1049,7 +1060,7 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 99118879 + oracle_price / 400 + 99118879 + oracle_price / 400 + 97859 ); let order_params_before = OrderParams { @@ -1069,7 +1080,7 @@ mod update_perp_auction_params { assert_ne!(order_params_before, order_params_after); assert_eq!( order_params_after.auction_start_price.unwrap(), - (99 * PRICE_PRECISION_U64 + 100000) as i64 + oracle_price / 400 + 18879 // use limit price and oracle buffer with some noise + (99 * PRICE_PRECISION_U64 + 100000) as i64 + oracle_price / 400 + 116738 // use limit price and oracle buffer with some noise ); let order_params_before = OrderParams { @@ -1088,11 +1099,11 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 99118879 + oracle_price / 400 + 99118879 + oracle_price / 400 + 97859 ); assert_eq!(order_params_after.auction_end_price.unwrap(), 98028211); - assert_eq!(order_params_after.auction_duration, Some(82)); + assert_eq!(order_params_after.auction_duration, Some(88)); let order_params_before = OrderParams { order_type: OrderType::Market, @@ -1110,11 +1121,11 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 98901080 - oracle_price / 400 + 98901080 - oracle_price / 400 + 117618 ); assert_eq!(order_params_after.auction_end_price.unwrap(), 100207026); - assert_eq!(order_params_after.auction_duration, Some(95)); + assert_eq!(order_params_after.auction_duration, Some(88)); } #[test] @@ -1325,8 +1336,11 @@ mod get_close_perp_params { let amm = AMM { last_ask_price_twap: 101 * PRICE_PRECISION_U64, last_bid_price_twap: 99 * PRICE_PRECISION_U64, + last_mark_price_twap_5min: 99 * PRICE_PRECISION_U64, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() }, mark_std: PRICE_PRECISION_U64, @@ -1385,7 +1399,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, PRICE_PRECISION_I64); + assert_eq!(auction_start_price, 2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 4 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, 4 * PRICE_PRECISION_I64 as i32); @@ -1420,7 +1434,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, -3 * PRICE_PRECISION_I64); + assert_eq!(auction_start_price, -2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 0); assert_eq!(oracle_price_offset, 0); @@ -1436,8 +1450,11 @@ mod get_close_perp_params { let amm = AMM { last_ask_price_twap: 101 * PRICE_PRECISION_U64, last_bid_price_twap: 99 * PRICE_PRECISION_U64, + last_mark_price_twap_5min: 100 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, ..HistoricalOracleData::default() }, mark_std: PRICE_PRECISION_U64, @@ -1462,7 +1479,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 1000000); + assert_eq!(auction_start_price, 0); assert_eq!(auction_end_price, -2 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, -2 * PRICE_PRECISION_I64 as i32); @@ -1498,7 +1515,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 3 * PRICE_PRECISION_I64); + assert_eq!(auction_start_price, 2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 0); assert_eq!(oracle_price_offset, 0); @@ -1536,7 +1553,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, -PRICE_PRECISION_I64); + assert_eq!(auction_start_price, -2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, -4 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, -4 * PRICE_PRECISION_I64 as i32); @@ -1613,7 +1630,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 641); + assert_eq!(auction_start_price, 284); assert_eq!(auction_end_price, -1021); assert_eq!(oracle_price_offset, -1021); diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 9871f39deb..56122778c6 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -10,7 +10,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::{self}; use crate::math::casting::Cast; #[cfg(test)] -use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; +use crate::math::constants::{ + AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64, +}; use crate::math::constants::{ AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, diff --git a/sdk/VERSION b/sdk/VERSION index b0c8a5df9a..9fb14cec6a 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.138.0-beta.9 \ No newline at end of file +2.140.0-beta.0 \ No newline at end of file diff --git a/sdk/bun.lock b/sdk/bun.lock index b97bf95c4b..c3292b4520 100644 --- a/sdk/bun.lock +++ b/sdk/bun.lock @@ -20,6 +20,7 @@ "@triton-one/yellowstone-grpc": "1.3.0", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", + "helius-laserstream": "0.1.8", "nanoid": "3.3.4", "node-cache": "5.1.2", "rpc-websockets": "7.5.1", @@ -335,6 +336,8 @@ "@types/node": ["@types/node@22.13.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ=="], + "@types/protobufjs": ["@types/protobufjs@6.0.0", "", { "dependencies": { "protobufjs": "*" } }, "sha512-A27RDExpAf3rdDjIrHKiJK6x8kqqJ4CmoChwtipfhVAn1p7+wviQFFP7dppn8FslSbHtQeVPvi8wNKkDjSYjHw=="], + "@types/semver": ["@types/semver@7.7.0", "", {}, "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA=="], "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], @@ -711,6 +714,20 @@ "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "helius-laserstream": ["helius-laserstream@0.1.8", "", { "dependencies": { "@types/protobufjs": "^6.0.0", "protobufjs": "^7.5.3" }, "optionalDependencies": { "helius-laserstream-darwin-arm64": "0.1.8", "helius-laserstream-darwin-x64": "0.1.8", "helius-laserstream-linux-arm64-gnu": "0.1.8", "helius-laserstream-linux-arm64-musl": "0.1.8", "helius-laserstream-linux-x64-gnu": "0.1.8", "helius-laserstream-linux-x64-musl": "0.1.8" }, "os": [ "linux", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-jXQkwQRWiowbVPGQrGacOkI5shKPhrEixCu93OpoMtL5fs9mnhtD7XKMPi8CX0W8gsqsJjwR4NlaR+EflyANbQ=="], + + "helius-laserstream-darwin-arm64": ["helius-laserstream-darwin-arm64@0.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p/K2Mi3wZnMxEYSLCvu858VyMvtJFonhdF8cLaMcszFv04WWdsK+hINNZpVRfakypvDfDPbMudEhL1Q9USD5+w=="], + + "helius-laserstream-darwin-x64": ["helius-laserstream-darwin-x64@0.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Hd5irFyfOqQZLdoj5a+OV7vML2YfySSBuKlOwtisMHkUuIXZ4NpAexslDmK7iP5VWRI+lOv9X/tA7BhxW7RGSQ=="], + + "helius-laserstream-linux-arm64-gnu": ["helius-laserstream-linux-arm64-gnu@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PlPm1dvOvTGBL1nuzK98Xe40BJq1JWNREXlBHKDVA/B+KCGQnIMJ1s6e1MevSvFE7SOix5i1BxhYIxGioK2GMg=="], + + "helius-laserstream-linux-arm64-musl": ["helius-laserstream-linux-arm64-musl@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-LFadfMRuTd1zo6RZqLTgHQapo3gJYioS7wFMWFoBOFulG0BpAqHEDNobkxx0002QArw+zX29MQ/5OaOCf8kKTA=="], + + "helius-laserstream-linux-x64-gnu": ["helius-laserstream-linux-x64-gnu@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-IZWK/OQIe0647QqfYikLb1DFK+nYtXLJiMcpj24qnNVWBOtMXmPc1hL6ebazdEiaKt7fxNd5IiM1RqeaqZAZMw=="], + + "helius-laserstream-linux-x64-musl": ["helius-laserstream-linux-x64-musl@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-riTS6VgxDae1fHOJ2XC/o/v1OZRbEv/3rcoa3NlAOnooDKp5HDgD0zJTcImjQHpYWwGaejx1oX/Ht53lxNoijw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -1195,6 +1212,8 @@ "@switchboard-xyz/on-demand/bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], + "@types/protobufjs/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], @@ -1221,6 +1240,8 @@ "glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "helius-laserstream/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], diff --git a/sdk/package.json b/sdk/package.json index 9f2067b649..bf99563351 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.138.0-beta.9", + "version": "2.140.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", @@ -55,6 +55,7 @@ "@triton-one/yellowstone-grpc": "1.3.0", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", + "helius-laserstream": "0.1.8", "nanoid": "3.3.4", "node-cache": "5.1.2", "rpc-websockets": "7.5.1", diff --git a/sdk/src/accounts/laserProgramAccountSubscriber.ts b/sdk/src/accounts/laserProgramAccountSubscriber.ts new file mode 100644 index 0000000000..a2315ad92f --- /dev/null +++ b/sdk/src/accounts/laserProgramAccountSubscriber.ts @@ -0,0 +1,215 @@ +import { GrpcConfigs, ResubOpts } from './types'; +import { Program } from '@coral-xyz/anchor'; +import { Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import * as Buffer from 'buffer'; +import { WebSocketProgramAccountSubscriber } from './webSocketProgramAccountSubscriber'; + +import { + LaserCommitmentLevel, + LaserSubscribe, + LaserstreamConfig, + LaserSubscribeRequest, + LaserSubscribeUpdate, + CompressionAlgorithms, + CommitmentLevel, +} from '../isomorphic/grpc'; + +type LaserCommitment = + (typeof LaserCommitmentLevel)[keyof typeof LaserCommitmentLevel]; + +export class LaserstreamProgramAccountSubscriber< + T, +> extends WebSocketProgramAccountSubscriber { + private stream: + | { + id: string; + cancel: () => void; + write?: (req: LaserSubscribeRequest) => Promise; + } + | undefined; + + private commitmentLevel: CommitmentLevel; + public listenerId?: number; + + private readonly laserConfig: LaserstreamConfig; + + private constructor( + laserConfig: LaserstreamConfig, + commitmentLevel: CommitmentLevel, + subscriptionName: string, + accountDiscriminator: string, + program: Program, + decodeBufferFn: (accountName: string, ix: Buffer) => T, + options: { filters: MemcmpFilter[] } = { filters: [] }, + resubOpts?: ResubOpts + ) { + super( + subscriptionName, + accountDiscriminator, + program, + decodeBufferFn, + options, + resubOpts + ); + this.laserConfig = laserConfig; + this.commitmentLevel = this.toLaserCommitment(commitmentLevel); + } + + public static async create( + grpcConfigs: GrpcConfigs, + subscriptionName: string, + accountDiscriminator: string, + program: Program, + decodeBufferFn: (accountName: string, ix: Buffer) => U, + options: { filters: MemcmpFilter[] } = { + filters: [], + }, + resubOpts?: ResubOpts + ): Promise> { + const laserConfig: LaserstreamConfig = { + apiKey: grpcConfigs.token, + endpoint: grpcConfigs.endpoint, + maxReconnectAttempts: grpcConfigs.enableReconnect ? 10 : 0, + channelOptions: { + 'grpc.default_compression_algorithm': CompressionAlgorithms.zstd, + 'grpc.max_receive_message_length': 1_000_000_000, + }, + }; + + const commitmentLevel = + grpcConfigs.commitmentLevel ?? CommitmentLevel.CONFIRMED; + + return new LaserstreamProgramAccountSubscriber( + laserConfig, + commitmentLevel, + subscriptionName, + accountDiscriminator, + program, + decodeBufferFn, + options, + resubOpts + ); + } + + async subscribe( + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void + ): Promise { + if (this.listenerId != null || this.isUnsubscribing) return; + + this.onChange = onChange; + + const filters = this.options.filters.map((filter) => { + return { + memcmp: { + offset: filter.memcmp.offset, + base58: filter.memcmp.bytes, + }, + }; + }); + + const request: LaserSubscribeRequest = { + slots: {}, + accounts: { + drift: { + account: [], + owner: [this.program.programId.toBase58()], + filters, + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + try { + const stream = await LaserSubscribe( + this.laserConfig, + request, + async (update: LaserSubscribeUpdate) => { + if (update.account) { + const slot = Number(update.account.slot); + const acc = update.account.account; + + const accountInfo = { + owner: new PublicKey(acc.owner), + lamports: Number(acc.lamports), + data: Buffer.Buffer.from(acc.data), + executable: acc.executable, + rentEpoch: Number(acc.rentEpoch), + }; + + const payload = { + accountId: new PublicKey(acc.pubkey), + accountInfo, + }; + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.handleRpcResponse({ slot }, payload); + this.setTimeout(); + } else { + this.handleRpcResponse({ slot }, payload); + } + } + }, + async (error) => { + console.error('LaserStream client error:', error); + throw error; + } + ); + + this.stream = stream; + this.listenerId = 1; + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + this.setTimeout(); + } + } catch (err) { + console.error('Failed to start LaserStream client:', err); + throw err; + } + } + + public async unsubscribe(onResub = false): Promise { + if (!onResub && this.resubOpts) { + this.resubOpts.resubTimeoutMs = undefined; + } + this.isUnsubscribing = true; + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + + if (this.listenerId != null && this.stream) { + try { + this.stream.cancel(); + } finally { + this.listenerId = undefined; + this.isUnsubscribing = false; + } + } else { + this.isUnsubscribing = false; + } + } + + public toLaserCommitment( + level: string | number | undefined + ): LaserCommitment { + if (typeof level === 'string') { + return ( + (LaserCommitmentLevel as any)[level.toUpperCase()] ?? + LaserCommitmentLevel.CONFIRMED + ); + } + return (level as LaserCommitment) ?? LaserCommitmentLevel.CONFIRMED; + } +} diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index 9a00d1270e..db9cf2112b 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -235,6 +235,7 @@ export type GrpcConfigs = { * Defaults to false, will throw on connection loss. */ enableReconnect?: boolean; + client?: 'yellowstone' | 'laser'; }; export interface HighLeverageModeConfigAccountSubscriber { diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index efe50f5b4c..f4233bb14a 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -18,8 +18,8 @@ import { AssetTier, SpotFulfillmentConfigStatus, IfRebalanceConfigParams, - AddAmmConstituentMappingDatum, TxParams, + AddAmmConstituentMappingDatum, SwapReduceOnly, InitializeConstituentParams, ConstituentStatus, @@ -48,6 +48,7 @@ import { getFuelOverflowAccountPublicKey, getTokenProgramForSpotMarket, getIfRebalanceConfigPublicKey, + getInsuranceFundStakeAccountPublicKey, getLpPoolPublicKey, getAmmConstituentMappingPublicKey, getConstituentTargetBasePublicKey, @@ -75,6 +76,7 @@ import { ONE, BASE_PRECISION, PRICE_PRECISION, + GOV_SPOT_MARKET_INDEX, } from './constants/numericConstants'; import { calculateTargetPriceTrade } from './math/trade'; import { calculateAmmReservesAfterSwap, getSwapDirection } from './math/amm'; @@ -4860,6 +4862,134 @@ export class AdminClient extends DriftClient { ); } + public async updateFeatureBitFlagsMedianTriggerPrice( + enable: boolean + ): Promise { + const updateFeatureBitFlagsMedianTriggerPriceIx = + await this.getUpdateFeatureBitFlagsMedianTriggerPriceIx(enable); + const tx = await this.buildTransaction( + updateFeatureBitFlagsMedianTriggerPriceIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsMedianTriggerPriceIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsMedianTriggerPrice( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateDelegateUserGovTokenInsuranceStake( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const updateDelegateUserGovTokenInsuranceStakeIx = + await this.getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority, + delegate + ); + + const tx = await this.buildTransaction( + updateDelegateUserGovTokenInsuranceStakeIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const marketIndex = GOV_SPOT_MARKET_INDEX; + const spotMarket = this.getSpotMarketAccount(marketIndex); + const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( + this.program.programId, + delegate, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + this.program.programId, + authority + ); + + const ix = + this.program.instruction.getUpdateDelegateUserGovTokenInsuranceStakeIx({ + accounts: { + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: ifStakeAccountPublicKey, + userStats: userStatsPublicKey, + signer: this.wallet.publicKey, + insuranceFundVault: spotMarket.insuranceFund.vault, + }, + }); + + return ix; + } + + public async depositIntoInsuranceFundStake( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey, + txParams?: TxParams + ): Promise { + const tx = await this.buildTransaction( + await this.getDepositIntoInsuranceFundStakeIx( + marketIndex, + amount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userTokenAccountPublicKey + ), + txParams + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositIntoInsuranceFundStakeIx( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey + ): Promise { + const spotMarket = this.getSpotMarketAccount(marketIndex); + return await this.program.instruction.depositIntoInsuranceFundStake( + marketIndex, + amount, + { + accounts: { + signer: this.wallet.publicKey, + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: insuranceFundStakePublicKey, + userStats: userStatsPublicKey, + spotMarketVault: spotMarket.vault, + insuranceFundVault: spotMarket.insuranceFund.vault, + userTokenAccount: userTokenAccountPublicKey, + tokenProgram: this.getTokenProgramForSpotMarket(spotMarket), + driftSigner: this.getSignerPublicKey(), + }, + } + ); + } + public async updateFeatureBitFlagsSettleLpPool( enable: boolean ): Promise { diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 03643e7d06..2e0f8aac86 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1312,6 +1312,19 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ oracleSource: OracleSource.PYTH_LAZER, pythLazerId: 1578, }, + { + fullName: 'ASTER', + category: ['DEX'], + symbol: 'ASTER-PERP', + baseAssetSymbol: 'ASTER', + marketIndex: 76, + oracle: new PublicKey('E4tyjB3os4jVczLVQ258uxLdcwjuqmhcsPquVWgrpah4'), + launchTs: 1758632629000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0xa903b5a82cb572397e3d47595d2889cf80513f5b4cf7a36b513ae10cc8b1e338', + pythLazerId: 2310, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 503607398c..3840a37d8a 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -129,6 +129,18 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ '0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a', pythLazerId: 7, }, + { + symbol: 'GLXY', + marketIndex: 7, + poolId: 0, + oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), + oracleSource: OracleSource.Prelaunch, + mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0x67e031d1723e5c89e4a826d80b2f3b41a91b05ef6122d523b8829a02e0f563aa', + }, ]; export const MainnetSpotMarkets: SpotMarketConfig[] = [ diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 830ff01611..0483cd314c 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -9250,7 +9250,7 @@ export class DriftClient { public async updateUserGovTokenInsuranceStake( authority: PublicKey, - txParams?: TxParams, + txParams?: TxParams ): Promise { const ix = await this.getUpdateUserGovTokenInsuranceStakeIx(authority); const tx = await this.buildTransaction(ix, txParams); diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 58912c99a5..79a3484def 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.137.0", + "version": "2.139.0", "name": "drift", "instructions": [ { @@ -1573,31 +1573,6 @@ } ] }, - { - "name": "updateUserAdvancedLp", - "accounts": [ - { - "name": "user", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "subAccountId", - "type": "u16" - }, - { - "name": "advancedLp", - "type": "bool" - } - ] - }, { "name": "updateUserProtectedMakerOrders", "accounts": [ @@ -2092,32 +2067,6 @@ ], "args": [] }, - { - "name": "updateUserOpenOrdersCount", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "filler", - "isMut": true, - "isSigner": false - }, - { - "name": "user", - "isMut": true, - "isSigner": false - } - ], - "args": [] - }, { "name": "adminDisableUpdatePerpBidAskTwap", "accounts": [ @@ -3141,6 +3090,42 @@ ], "args": [] }, + { + "name": "updateDelegateUserGovTokenInsuranceStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializeInsuranceFundStake", "accounts": [ @@ -3390,62 +3375,79 @@ ] }, { - "name": "transferProtocolIfShares", + "name": "beginInsuranceFundSwap", "accounts": [ { - "name": "signer", + "name": "state", "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, "isSigner": true }, { - "name": "transferConfig", + "name": "outInsuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "state", - "isMut": false, + "name": "inInsuranceFundVault", + "isMut": true, "isSigner": false }, { - "name": "spotMarket", + "name": "outTokenAccount", "isMut": true, "isSigner": false }, { - "name": "insuranceFundStake", + "name": "inTokenAccount", "isMut": true, "isSigner": false }, { - "name": "userStats", + "name": "ifRebalanceConfig", "isMut": true, "isSigner": false }, { - "name": "authority", + "name": "tokenProgram", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "insuranceFundVault", + "name": "driftSigner", "isMut": false, "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] } ], "args": [ { - "name": "marketIndex", + "name": "inMarketIndex", "type": "u16" }, { - "name": "shares", - "type": "u128" + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" } ] }, { - "name": "beginInsuranceFundSwap", + "name": "endInsuranceFundSwap", "accounts": [ { "name": "state", @@ -3509,15 +3511,11 @@ { "name": "outMarketIndex", "type": "u16" - }, - { - "name": "amountIn", - "type": "u64" } ] }, { - "name": "endInsuranceFundSwap", + "name": "transferProtocolIfSharesToRevenuePool", "accounts": [ { "name": "state", @@ -3530,22 +3528,12 @@ "isSigner": true }, { - "name": "outInsuranceFundVault", - "isMut": true, - "isSigner": false - }, - { - "name": "inInsuranceFundVault", - "isMut": true, - "isSigner": false - }, - { - "name": "outTokenAccount", + "name": "insuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "inTokenAccount", + "name": "spotMarketVault", "isMut": true, "isSigner": false }, @@ -3563,42 +3551,44 @@ "name": "driftSigner", "isMut": false, "isSigner": false - }, - { - "name": "instructions", - "isMut": false, - "isSigner": false, - "docs": [ - "Instructions Sysvar for instruction introspection" - ] } ], "args": [ { - "name": "inMarketIndex", + "name": "marketIndex", "type": "u16" }, { - "name": "outMarketIndex", - "type": "u16" + "name": "amount", + "type": "u64" } ] }, { - "name": "transferProtocolIfSharesToRevenuePool", + "name": "depositIntoInsuranceFundStake", "accounts": [ { - "name": "state", + "name": "signer", "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, "isSigner": false }, { - "name": "authority", + "name": "spotMarket", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "insuranceFundVault", + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", "isMut": true, "isSigner": false }, @@ -3608,7 +3598,12 @@ "isSigner": false }, { - "name": "ifRebalanceConfig", + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", "isMut": true, "isSigner": false }, @@ -4272,27 +4267,6 @@ } ] }, - { - "name": "updateSerumVault", - "accounts": [ - { - "name": "state", - "isMut": true, - "isSigner": false - }, - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "srmVault", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "initializePerpMarket", "accounts": [ @@ -7044,76 +7018,6 @@ } ] }, - { - "name": "initializeProtocolIfSharesTransferConfig", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "protocolIfSharesTransferConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "updateProtocolIfSharesTransferConfig", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "protocolIfSharesTransferConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "whitelistedSigners", - "type": { - "option": { - "array": [ - "publicKey", - 4 - ] - } - } - }, - { - "name": "maxTransferPerEpoch", - "type": { - "option": "u128" - } - } - ] - }, { "name": "initializePrelaunchOracle", "accounts": [ @@ -14896,6 +14800,9 @@ }, { "name": "StakeTransfer" + }, + { + "name": "AdminDeposit" } ] } diff --git a/sdk/src/isomorphic/grpc.node.ts b/sdk/src/isomorphic/grpc.node.ts index 4d58f734d1..907bdcc432 100644 --- a/sdk/src/isomorphic/grpc.node.ts +++ b/sdk/src/isomorphic/grpc.node.ts @@ -2,10 +2,19 @@ import type Client from '@triton-one/yellowstone-grpc'; import type { SubscribeRequest, SubscribeUpdate, - CommitmentLevel, } from '@triton-one/yellowstone-grpc'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; import { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; +import { + CommitmentLevel as LaserCommitmentLevel, + subscribe as LaserSubscribe, + LaserstreamConfig, + SubscribeRequest as LaserSubscribeRequest, + SubscribeUpdate as LaserSubscribeUpdate, + CompressionAlgorithms, +} from 'helius-laserstream'; + export { ClientDuplexStream, ChannelOptions, @@ -13,6 +22,12 @@ export { SubscribeUpdate, CommitmentLevel, Client, + LaserSubscribe, + LaserCommitmentLevel, + LaserstreamConfig, + LaserSubscribeRequest, + LaserSubscribeUpdate, + CompressionAlgorithms, }; // Export a function to create a new Client instance diff --git a/sdk/src/math/auction.ts b/sdk/src/math/auction.ts index 5a58f60054..cabf5f4860 100644 --- a/sdk/src/math/auction.ts +++ b/sdk/src/math/auction.ts @@ -26,6 +26,10 @@ export function isFallbackAvailableLiquiditySource( return true; } + if ((order.bitFlags & OrderBitFlag.SafeTriggerOrder) !== 0) { + return true; + } + return new BN(slot).sub(order.slot).gt(new BN(minAuctionDuration)); } diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 0ef192717e..5bfa6370bd 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -245,7 +245,7 @@ export function isFillableByVAMM( market, mmOraclePriceData, slot - ).gte(market.amm.minOrderSize)) || + ).gt(ZERO)) || isOrderExpired(order, ts) ); } diff --git a/sdk/src/orderSubscriber/grpcSubscription.ts b/sdk/src/orderSubscriber/grpcSubscription.ts index 41101435ab..04160c0bff 100644 --- a/sdk/src/orderSubscriber/grpcSubscription.ts +++ b/sdk/src/orderSubscriber/grpcSubscription.ts @@ -5,6 +5,7 @@ import { OrderSubscriber } from './OrderSubscriber'; import { GrpcConfigs, ResubOpts } from '../accounts/types'; import { UserAccount } from '../types'; import { getUserFilter, getNonIdleUserFilter } from '../memcmp'; +import { LaserstreamProgramAccountSubscriber } from '../accounts/laserProgramAccountSubscriber'; export class grpcSubscription { private orderSubscriber: OrderSubscriber; @@ -12,7 +13,9 @@ export class grpcSubscription { private resubOpts?: ResubOpts; private resyncIntervalMs?: number; - private subscriber?: grpcProgramAccountSubscriber; + private subscriber?: + | grpcProgramAccountSubscriber + | LaserstreamProgramAccountSubscriber; private resyncTimeoutId?: ReturnType; private decoded?: boolean; @@ -47,17 +50,32 @@ export class grpcSubscription { return; } - this.subscriber = await grpcProgramAccountSubscriber.create( - this.grpcConfigs, - 'OrderSubscriber', - 'User', - this.orderSubscriber.driftClient.program, - this.orderSubscriber.decodeFn, - { - filters: [getUserFilter(), getNonIdleUserFilter()], - }, - this.resubOpts - ); + if (this.grpcConfigs.client === 'laser') { + this.subscriber = + await LaserstreamProgramAccountSubscriber.create( + this.grpcConfigs, + 'OrderSubscriber', + 'User', + this.orderSubscriber.driftClient.program, + this.orderSubscriber.decodeFn, + { + filters: [getUserFilter(), getNonIdleUserFilter()], + }, + this.resubOpts + ); + } else { + this.subscriber = await grpcProgramAccountSubscriber.create( + this.grpcConfigs, + 'OrderSubscriber', + 'User', + this.orderSubscriber.driftClient.program, + this.orderSubscriber.decodeFn, + { + filters: [getUserFilter(), getNonIdleUserFilter()], + }, + this.resubOpts + ); + } await this.subscriber.subscribe( ( diff --git a/sdk/src/swift/swiftOrderSubscriber.ts b/sdk/src/swift/swiftOrderSubscriber.ts index bab3032795..540691521d 100644 --- a/sdk/src/swift/swiftOrderSubscriber.ts +++ b/sdk/src/swift/swiftOrderSubscriber.ts @@ -41,6 +41,31 @@ export type SwiftOrderSubscriberConfig = { keypair: Keypair; }; +/** + * Swift order message received from WebSocket + */ +export interface SwiftOrderMessage { + /** Hex string of the order message */ + order_message: string; + /** Base58 string of taker authority */ + taker_authority: string; + /** Base58 string of signing authority */ + signing_authority: string; + /** Base64 string containing the order signature */ + order_signature: string; + /** Swift order UUID */ + uuid: string; + /** Whether the order auction params are likely to be sanitized on submission to program */ + will_sanitize?: boolean; + /** Base64 string of a prerequisite deposit tx. The swift order_message should be bundled + * after the deposit when present */ + depositTx?: string; + /** order market index */ + market_index: number; + /** order timestamp in unix ms */ + ts: number; +} + export class SwiftOrderSubscriber { private heartbeatTimeout: ReturnType | null = null; private readonly heartbeatIntervalMs = 60000; @@ -48,7 +73,7 @@ export class SwiftOrderSubscriber { private driftClient: DriftClient; public userAccountGetter?: AccountGetter; // In practice, this for now is just an OrderSubscriber or a UserMap public onOrder: ( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, @@ -120,13 +145,14 @@ export class SwiftOrderSubscriber { async subscribe( onOrder: ( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, isDelegateSigner?: boolean ) => Promise, - acceptSanitized = false + acceptSanitized = false, + acceptDepositTrade = false ): Promise { this.onOrder = onOrder; @@ -150,13 +176,20 @@ export class SwiftOrderSubscriber { } if (message['order']) { - const order = message['order']; + const order = message['order'] as SwiftOrderMessage; // ignore likely sanitized orders by default - if (order['will_sanitize'] === true && !acceptSanitized) { + if (order.will_sanitize === true && !acceptSanitized) { return; } + // order has a prerequisite deposit tx attached + if (message['deposit']) { + order.depositTx = message['deposit']; + if (!acceptDepositTrade) { + return; + } + } const signedMsgOrderParamsBuf = Buffer.from( - order['order_message'], + order.order_message, 'hex' ); const isDelegateSigner = signedMsgOrderParamsBuf @@ -224,7 +257,7 @@ export class SwiftOrderSubscriber { } async getPlaceAndMakeSignedMsgOrderIxs( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMsgOrderParamsMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, @@ -235,7 +268,7 @@ export class SwiftOrderSubscriber { } const signedMsgOrderParamsBuf = Buffer.from( - orderMessageRaw['order_message'], + orderMessageRaw.order_message, 'hex' ); @@ -256,10 +289,8 @@ export class SwiftOrderSubscriber { isDelegateSigner ); - const takerAuthority = new PublicKey(orderMessageRaw['taker_authority']); - const signingAuthority = new PublicKey( - orderMessageRaw['signing_authority'] - ); + const takerAuthority = new PublicKey(orderMessageRaw.taker_authority); + const signingAuthority = new PublicKey(orderMessageRaw.signing_authority); const takerUserPubkey = isDelegateSigner ? (signedMessage as SignedMsgOrderParamsDelegateMessage).takerPubkey : await getUserAccountPublicKey( @@ -273,9 +304,9 @@ export class SwiftOrderSubscriber { const ixs = await this.driftClient.getPlaceAndMakeSignedMsgPerpOrderIxs( { orderParams: signedMsgOrderParamsBuf, - signature: Buffer.from(orderMessageRaw['order_signature'], 'base64'), + signature: Buffer.from(orderMessageRaw.order_signature, 'base64'), }, - decodeUTF8(orderMessageRaw['uuid']), + decodeUTF8(orderMessageRaw.uuid), { taker: takerUserPubkey, takerUserAccount, diff --git a/sdk/yarn.lock b/sdk/yarn.lock index 4013c2cc05..c3130ae226 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -1281,6 +1281,13 @@ dependencies: undici-types "~5.26.4" +"@types/protobufjs@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/protobufjs/-/protobufjs-6.0.0.tgz#aeabb43f9507bb19c8adfb479584c151082353e4" + integrity sha512-A27RDExpAf3rdDjIrHKiJK6x8kqqJ4CmoChwtipfhVAn1p7+wviQFFP7dppn8FslSbHtQeVPvi8wNKkDjSYjHw== + dependencies: + protobufjs "*" + "@types/semver@^7.5.0": version "7.7.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" @@ -2820,6 +2827,51 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +helius-laserstream-darwin-arm64@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-arm64/-/helius-laserstream-darwin-arm64-0.1.8.tgz#d78ad15e6cd16dc9379a9a365f9fcb3f958e6c01" + integrity sha512-p/K2Mi3wZnMxEYSLCvu858VyMvtJFonhdF8cLaMcszFv04WWdsK+hINNZpVRfakypvDfDPbMudEhL1Q9USD5+w== + +helius-laserstream-darwin-x64@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-x64/-/helius-laserstream-darwin-x64-0.1.8.tgz#e57bc8f03135fd3b5c01a5aebd7b87c42129da50" + integrity sha512-Hd5irFyfOqQZLdoj5a+OV7vML2YfySSBuKlOwtisMHkUuIXZ4NpAexslDmK7iP5VWRI+lOv9X/tA7BhxW7RGSQ== + +helius-laserstream-linux-arm64-gnu@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-gnu/-/helius-laserstream-linux-arm64-gnu-0.1.8.tgz#1b3c8440804d143f650166842620fc334f9c319b" + integrity sha512-PlPm1dvOvTGBL1nuzK98Xe40BJq1JWNREXlBHKDVA/B+KCGQnIMJ1s6e1MevSvFE7SOix5i1BxhYIxGioK2GMg== + +helius-laserstream-linux-arm64-musl@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-musl/-/helius-laserstream-linux-arm64-musl-0.1.8.tgz#28e0645bebc3253d2a136cf0bd13f8cb5256f47b" + integrity sha512-LFadfMRuTd1zo6RZqLTgHQapo3gJYioS7wFMWFoBOFulG0BpAqHEDNobkxx0002QArw+zX29MQ/5OaOCf8kKTA== + +helius-laserstream-linux-x64-gnu@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-gnu/-/helius-laserstream-linux-x64-gnu-0.1.8.tgz#e59990ca0bcdc27e46f71a8fc2c18fddbe6f07e3" + integrity sha512-IZWK/OQIe0647QqfYikLb1DFK+nYtXLJiMcpj24qnNVWBOtMXmPc1hL6ebazdEiaKt7fxNd5IiM1RqeaqZAZMw== + +helius-laserstream-linux-x64-musl@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-musl/-/helius-laserstream-linux-x64-musl-0.1.8.tgz#42aa0919ef266c40f50ac74d6f9d871d4e2e7c9c" + integrity sha512-riTS6VgxDae1fHOJ2XC/o/v1OZRbEv/3rcoa3NlAOnooDKp5HDgD0zJTcImjQHpYWwGaejx1oX/Ht53lxNoijw== + +helius-laserstream@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream/-/helius-laserstream-0.1.8.tgz#6ee5e0bc9fe2560c03a0d2c9079b9f875c3e6bb7" + integrity sha512-jXQkwQRWiowbVPGQrGacOkI5shKPhrEixCu93OpoMtL5fs9mnhtD7XKMPi8CX0W8gsqsJjwR4NlaR+EflyANbQ== + dependencies: + "@types/protobufjs" "^6.0.0" + protobufjs "^7.5.3" + optionalDependencies: + helius-laserstream-darwin-arm64 "0.1.8" + helius-laserstream-darwin-x64 "0.1.8" + helius-laserstream-linux-arm64-gnu "0.1.8" + helius-laserstream-linux-arm64-musl "0.1.8" + helius-laserstream-linux-x64-gnu "0.1.8" + helius-laserstream-linux-x64-musl "0.1.8" + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -3673,6 +3725,24 @@ pretty-ms@^7.0.1: dependencies: parse-ms "^2.1.0" +protobufjs@*, protobufjs@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protobufjs@^7.2.5, protobufjs@^7.4.0: version "7.5.3" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.3.tgz#13f95a9e3c84669995ec3652db2ac2fb00b89363" diff --git a/tests/insuranceFundStake.ts b/tests/insuranceFundStake.ts index c90a1795ce..0f67d1814c 100644 --- a/tests/insuranceFundStake.ts +++ b/tests/insuranceFundStake.ts @@ -29,6 +29,7 @@ import { unstakeSharesToAmount, MarketStatus, LIQUIDATION_PCT_PRECISION, + getUserStatsAccountPublicKey, } from '../sdk/src'; import { @@ -40,6 +41,7 @@ import { sleep, mockOracleNoProgram, setFeedPriceNoProgram, + mintUSDCToUser, } from './testHelpers'; import { ContractTier, PERCENTAGE_PRECISION, UserStatus } from '../sdk'; import { startAnchor } from 'solana-bankrun'; @@ -1163,6 +1165,33 @@ describe('insurance fund stake', () => { // assert(usdcBefore.eq(usdcAfter)); }); + it('admin deposit into insurance fund stake', async () => { + await mintUSDCToUser( + usdcMint, + userUSDCAccount.publicKey, + usdcAmount, + bankrunContextWrapper + ); + const marketIndex = 0; + const insuranceFundStakePublicKey = getInsuranceFundStakeAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey + ); + const txSig = await driftClient.depositIntoInsuranceFundStake( + marketIndex, + usdcAmount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userUSDCAccount.publicKey + ); + bankrunContextWrapper.printTxLogs(txSig); + }); + // it('settle spotMarket to insurance vault', async () => { // const marketIndex = new BN(0); diff --git a/yarn.lock b/yarn.lock index 41c9b1a9d6..2678bc1b59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -895,12 +895,12 @@ anchor-bankrun@0.3.0: resolved "https://registry.yarnpkg.com/anchor-bankrun/-/anchor-bankrun-0.3.0.tgz#3789fcecbc201a2334cff228b99cc0da8ef0167e" integrity sha512-PYBW5fWX+iGicIS5MIM/omhk1tQPUc0ELAnI/IkLKQJ6d75De/CQRh8MF2bU/TgGyFi6zEel80wUe3uRol9RrQ== -ansi-regex@^5.0.1: +ansi-regex@5.0.1, ansi-regex@^5.0.1, ansi-regex@^6.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@4.3.0, ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -938,6 +938,11 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -967,6 +972,11 @@ axios@^1.5.1, axios@^1.8.3, axios@^1.9.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +backslash@<0.2.1: + version "0.2.0" + resolved "https://registry.yarnpkg.com/backslash/-/backslash-0.2.0.tgz#6c3c1fce7e7e714ccfc10fd74f0f73410677375f" + integrity sha512-Avs+8FUZ1HF/VFP4YWwHQZSGzRPm37ukU1JQYQWijuHhtXdOuAzcZ8PcAzfIw898a8PyBzdn+RtnKA6MzW0X2A== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1167,7 +1177,14 @@ chai@4.4.1: pathval "^1.1.1" type-detect "^4.0.8" -chalk@^4.0.0, chalk@~4.1.2: +chalk-template@<1.1.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-1.1.0.tgz#ffc55db6dd745e9394b85327c8ac8466edb7a7b1" + integrity sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg== + dependencies: + chalk "^5.2.0" + +chalk@4.1.2, chalk@^4.0.0, chalk@^5.2.0, chalk@^5.3.0, chalk@^5.4.1, chalk@~4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1175,11 +1192,6 @@ chalk@^4.0.0, chalk@~4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.3.0, chalk@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" - integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== - check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -1196,17 +1208,24 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== +color-convert@<3.1.1, color-convert@^2.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-3.1.0.tgz#ce16ebb832f9d7522649ed9e11bc0ccb9433a524" + integrity sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA== dependencies: - color-name "~1.1.4" + color-name "^2.0.0" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@<2.0.1, color-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.0.0.tgz#03ff6b1b5aec9bb3cf1ed82400c2790dfcd01d2d" + integrity sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow== + +color-string@<2.1.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-2.1.0.tgz#a1cc4bb16a23032ff1048a2458a170323b15a23f" + integrity sha512-gNVoDzpaSwvftp6Y8nqk97FtZoXP9Yj7KGYB8yIXuv0JcfqbYihTrd1OU5iZW9btfXde4YAOCRySBHT7O910MA== + dependencies: + color-name "^2.0.0" combined-stream@^1.0.8: version "1.0.8" @@ -1270,7 +1289,7 @@ csvtojson@2.0.10: lodash "^4.17.3" strip-bom "^2.0.0" -debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@<4.4.2, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.4.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== @@ -1373,6 +1392,13 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +error-ex@<1.3.3: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -1761,11 +1787,23 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +has-ansi@<6.0.1: + version "6.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-6.0.0.tgz#8118b2fb548c062f9356c7d5013b192a238ce3b3" + integrity sha512-1AYj+gqAskFf9Skb7xuEYMfJqkW3TJ8lukw4Fczw+Y6jRkgxvcE4JiFWuTO4DsoleMvvHudryolA9ObJHJKHWQ== + dependencies: + ansi-regex "^6.0.1" + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-flag@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-5.0.1.tgz#5483db2ae02a472d1d0691462fc587d1843cd940" + integrity sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA== + has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" @@ -1848,6 +1886,11 @@ is-arguments@^1.0.4: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-arrayish@<0.3.3, is-arrayish@^0.2.1, is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -2491,11 +2534,27 @@ shiki@^0.11.1: vscode-oniguruma "^1.6.1" vscode-textmate "^6.0.0" +simple-swizzle@<0.2.3: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -2572,7 +2631,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -2619,13 +2678,21 @@ superstruct@^2.0.2: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== -supports-color@^7.1.0: +supports-color@7.2.0, supports-color@^10.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" +supports-hyperlinks@<4.1.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-4.1.0.tgz#f006d9e2f6b9b6672f86c86c6f76bf52a69f4d91" + integrity sha512-6lY0rDZ5bbZhAPrwpz/nMR6XmeaFmh2itk7YnIyph2jblPmDcKMCPkSdLFTlaX8snBvg7OJmaOL3WRLqMEqcJQ== + dependencies: + has-flag "^5.0.1" + supports-color "^10.0.0" + text-encoding-utf-8@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" @@ -2803,7 +2870,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrap-ansi@^7.0.0: +wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 8757c9335abcc7bbaacc55acc8f2e37b0312e712 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:48:59 -0700 Subject: [PATCH 088/159] add new bulk instruction packaging --- sdk/src/driftClient.ts | 90 +++++++++++++++++++++++++++++------------- tests/lpPoolSwap.ts | 15 +------ 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0483cd314c..acbcaab506 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -10478,15 +10478,7 @@ export class DriftClient { inAmount: BN, minOutAmount: BN, lpPool: PublicKey, - constituentTargetBase: PublicKey, - constituentInTokenAccount: PublicKey, - constituentOutTokenAccount: PublicKey, - userInTokenAccount: PublicKey, - userOutTokenAccount: PublicKey, - inConstituent: PublicKey, - outConstituent: PublicKey, - inMarketMint: PublicKey, - outMarketMint: PublicKey, + userAuthority: PublicKey, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( @@ -10497,15 +10489,7 @@ export class DriftClient { inAmount, minOutAmount, lpPool, - constituentTargetBase, - constituentInTokenAccount, - constituentOutTokenAccount, - userInTokenAccount, - userOutTokenAccount, - inConstituent, - outConstituent, - inMarketMint, - outMarketMint + userAuthority ), txParams ), @@ -10521,21 +10505,49 @@ export class DriftClient { inAmount: BN, minOutAmount: BN, lpPool: PublicKey, - constituentTargetBase: PublicKey, - constituentInTokenAccount: PublicKey, - constituentOutTokenAccount: PublicKey, - userInTokenAccount: PublicKey, - userOutTokenAccount: PublicKey, - inConstituent: PublicKey, - outConstituent: PublicKey, - inMarketMint: PublicKey, - outMarketMint: PublicKey + userAuthority: PublicKey ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [], readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], }); + const constituentInTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + const constituentOutTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const userInTokenAccount = await getAssociatedTokenAddress( + this.getSpotMarketAccount(inMarketIndex).mint, + userAuthority + ); + const userOutTokenAccount = await getAssociatedTokenAddress( + this.getSpotMarketAccount(outMarketIndex).mint, + userAuthority + ); + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inMarketMint = this.getSpotMarketAccount(inMarketIndex).mint; + const outMarketMint = this.getSpotMarketAccount(outMarketIndex).mint; + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + return this.program.instruction.lpPoolSwap( inMarketIndex, outMarketIndex, @@ -11245,6 +11257,30 @@ export class DriftClient { return ixs; } + async getAllLpPoolSwapIxs( + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + userAuthority: PublicKey + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push(...(await this.getAllUpdateLpPoolAumIxs(lpPool, constituentMap))); + ixs.push( + await this.getLpPoolSwapIx( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + lpPool.pubkey, + userAuthority + ) + ); + return ixs; + } + async settlePerpToLpPool( lpPoolName: number[], perpMarketIndexes: number[] diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 17f9d03c89..7cfc3e0c87 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -425,11 +425,6 @@ describe('LP Pool', () => { expect(lpPool2.lastAum.gt(lpPool1.lastAum)).to.be.true; console.log(`AUM: ${convertToNumber(lpPool2.lastAum, QUOTE_PRECISION)}`); - const constituentTargetWeightsPublicKey = getConstituentTargetBasePublicKey( - program.programId, - lpPoolKey - ); - // swap c0 for c1 const adminAuth = adminClient.wallet.publicKey; @@ -467,15 +462,7 @@ describe('LP Pool', () => { new BN(224_300_000), new BN(0), lpPoolKey, - constituentTargetWeightsPublicKey, - const0TokenAccount, - const1TokenAccount, - c0UserTokenAccount, - c1UserTokenAccount, - const0Key, - const1Key, - usdcMint.publicKey, - spotTokenMint.publicKey + adminAuth ) ); From e3d08a096f15df0f9850a5c83849886d6b58f0a3 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:34:42 -0700 Subject: [PATCH 089/159] logging changes --- programs/drift/src/controller/spot_balance.rs | 2 -- programs/drift/src/instructions/lp_pool.rs | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/controller/spot_balance.rs b/programs/drift/src/controller/spot_balance.rs index 100442566e..6e34edb369 100644 --- a/programs/drift/src/controller/spot_balance.rs +++ b/programs/drift/src/controller/spot_balance.rs @@ -250,12 +250,10 @@ pub fn update_spot_balances( } if token_amount > 0 { - msg!("token amount to transfer: {}", token_amount); spot_balance.update_balance_type(*update_direction)?; let round_up = update_direction == &SpotBalanceType::Borrow; let balance_delta = get_spot_balance(token_amount, spot_market, update_direction, round_up)?; - msg!("balance delta {}", balance_delta); spot_balance.increase_balance(balance_delta)?; increase_spot_balance(balance_delta, spot_market, update_direction)?; } diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 7971aeb26c..49f4f4f56a 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -1590,7 +1590,8 @@ fn transfer_from_program_vault<'info>( validate!( balance_diff_notional <= PRICE_PRECISION_I64 / 100, ErrorCode::LpInvariantFailed, - "Constituent balance mismatch after withdraw from program vault" + "Constituent balance mismatch after withdraw from program vault, {}", + balance_diff_notional )?; Ok(()) From ecd8d3a550f26befd896e9c314062bc54089ac58 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 29 Sep 2025 14:05:23 -0700 Subject: [PATCH 090/159] Wphan/master-dlp (#1918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix comments (#1844) * chore: update laser 0.1.8 * chore: remove logging * program: tweak ResizeSignedMsgUserOrders (#1898) * fix linter and cargo test * fix cargo build errors * v2.138.0 * sdk: release v2.139.0-beta.0 * program: init-delegated-if-stake (#1859) * program: init-delegated-if-stake * add sdk * CHANGELOG --------- Co-authored-by: Chris Heaney * program: auction-order-params-on-slow-fast-twap-divergence (#1882) * program: auction-order-params-on-slow-fast-twap-divergence * change tests * rm dlog * CHANGELOG * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: add invariant for max in amount for if swap (#1825) * sdk: release v2.139.0-beta.1 * chore: add grpc client to order subscriber * sdk: release v2.139.0-beta.2 * sdk: add market index 76 to constant (#1901) * sdk: release v2.139.0-beta.3 * fix ui build (#1902) * sdk: release v2.139.0-beta.4 * sdk: update aster config (#1903) * update aster config * add pythLazerId * sdk: release v2.139.0-beta.5 * Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#1907) This reverts commit 0a8e15349f45e135df3eb2341f163d70ef09fe64. * sdk: release v2.139.0-beta.6 * Revert "Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#…" (#1910) * sdk: release v2.139.0-beta.7 * more robust isDelegateSigner for swift orders * sdk: release v2.139.0-beta.8 * program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit (#1909) * program: update-resolve-perp-pnl-pool-validate * CHANGELOG --------- Co-authored-by: Chris Heaney * program: add immutable owner support for token 22 vaults (#1904) * program: add immutable owner support for token 22 vaults * cargo fmt -- * CHANGELOG * sdk: tweak math for filling triggers (#1880) * sdk: tweak math for filling triggers * add back line * sdk: release v2.139.0-beta.9 * program: allow delegate to update user position max margin ratio (#1913) * Revert "more robust isDelegateSigner for swift orders" This reverts commit 2d4e30b5bfac835c2251b8640b898408714a7c13. * sdk: release v2.139.0-beta.10 * update SwiftOrderMessage type for missing fields (#1908) * sdk: release v2.139.0-beta.11 * sdk: add getUpdateFeatureBitFlagsMedianTriggerPriceIx * sdk: release v2.139.0-beta.12 * update devnet market constants (#1914) * sdk: release v2.139.0-beta.13 * program: deposit into if stake from admin (#1899) * program: deposit into if stake from admin * add test * change action * cargo fmt -- * move depositIntoInsuranceFundStake to adminClient --------- Co-authored-by: wphan * sdk: release v2.139.0-beta.14 * program: comment out unused ix (#1911) * program: raise MAX_BASE_ASSET_AMOUNT_WITH_AMM numerical invariant * v2.139.0 * sdk: release v2.140.0-beta.0 * sdk: update constants market index 77 (#1916) * sdk: release v2.140.0-beta.1 * Wphan/builder codes (#1805) * program: init lp pool * cargo fmt -- * add total fee fields * add update_target_weights math * program: use sparse matrix for constituent map and update tests * zero copy accounts, init ix (#1578) * update accounts (#1580) * zero copy + permissionless crank ixs (#1581) * program: support negative target weights for borrow-lend * fix tests to work with zero copy * few comment changes * remove discriminator from impl macro * add get_swap_amount, get_swap_fees, get_weight (#1579) * add get_swap_amount, get_swap_fees, get_weight * update accounts * add back ts * rebase * add constituent swap fees * fix swap fee calc (#1582) * add init amm mapping to lp context (#1583) * init constituent * add initializeLpPool test (#1585) * add initializeLpPool test * add check for constituent target weights * add add datum ix * add init tests and invariant checks * rename data to more useful names * dlp use spl token program (#1588) * add crank ix * update total_weight for validation_flags check * push test so far * overriding perp position works * remove message * fix dup total_weight add * constituent map remaining accounts * compiles * bankrun tests pass * compiles but casting failure in overflow protection test * address comment and change token arguments from u64 to u128 * bankrun tests pass * init constituent token account (#1596) * update aum calc * add update /remove mapping ixs * fix test - init constituent spot market * add crank improvements * passes tests * precision fix crank aum * precision fixes and constituent map check for account owner * add passthrough account logic (#1602) * add passthrough account logic * cant read yet * fix all zc alignment issues * make oracle source a u8 on zc struct * Wphan/dlp-swap-ixs (#1592) * add lp_swap ix * rebase * test helpers * swap works * fix swaps, add more cargo tests for fees n swap amt * remove console.logs * address PR comments * merge upstream * post-merge fixes * store bumps on accounts (#1604) * store bumps on accounts * do pda check in constituent map * address comments * Wphan/add liquidity (#1607) * add add remove liquidity fees calc * add liquidity ix * fix init mint and lppool token account, refactor test fees * add removeLiquidity bankrun test * merge upstream * add LPPool.next_mint_redeem_id * program: lp-pool-to-use-target-base-vector (#1615) * init lp pool target-base matrix * working target-base logic * add todos for add/remove liquidity aum * add renames + fix test * add beta and cost to trade in bps to target datum * add more tests * add fields to LP events, fix tests (#1620) * add fields to LP events, fix tests * revert target weight calc * add constituent.next_swap_id, fix cost_to_trade math * dlp jup swap (#1636) * dlp jup swap * add admin client ixs * almost fixed * test working? * update begin and end swap * tweaks * fix math on how much was swapped * remove unnecessary lp pool args * extra account validation * added token account pda checks in other ixs * stablecoin targets (#1638) * is stablecoin * address comments --------- Co-authored-by: Chris Heaney * cleanup * transfer oracle data ix to constituent (#1643) * transfer oracle data ix to constituent * add lib entrypoint * simplify more * add spot market constraint * big cargo test (#1644) * derivative constituents + better testing + bug fixes (#1657) * all tests technically pass * update tests + prettify * bug fixes and tests pass * fix many bugs and finalize logic * deposit/borrow working and changing positions (#1652) * sdk: allow custom coder * program: dlp add upnl for settles to amm cache (#1659) * program: dlp add-upnl-for-settles-to-amm-cache * finish up lp pool transfer from perp market * add amount_to_transfer using diff * merge * add pnl and fee pool accounting + transfer from dlp to perp market --------- Co-authored-by: Nour Alharithi * remove unused accounts coder * move customCoder into sdk, lint * testing: ix: settle perp to dlp, insufficient balance edge case and improvements (#1688) * finish edge case test * aum check also passes * prettify * added more settle test coverage and squash bugs (#1689) * dlp: add constituentMap (#1699) * Nour/gauntlet fee impl (#1698) * added correlation matrix infra * refactor builds * mint redeem handled for usdc * remove liquidity also should work * all tests pass * bankrun tests pass too * update aum considers amm cache (#1701) * prettify (#1702) * Wphan/merge master dlp (#1703) * feat: init swift user orders on user account creation if needed * fix: wrong pushing of swift user orders ixs * fix: broken swift tests * fix: swift -> signed msg * refactor(sdk): update jupiter's api url * fix(sdk): remove error thrown * indicative qutoes server changes * sdk: release v2.121.0-beta.7 * sdK: update market index 33 oracle rr (#1606) * sdk: add to spot constants market index 34 * revert adminClient.ts change * sdk: update spot market constants oracle index 33 * sdk: release v2.121.0-beta.8 * sdk: high leverage mode updates (#1605) * sdk: high leverage mode updates * add optional param for fee calc * update changelog * sdk: release v2.121.0-beta.9 * getPlaceSignedMsgTakerPerpOrderIxs infer HLM mode from bitflags (#1608) * sdk: release v2.121.0-beta.10 * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs (#1610) * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs * bankrun test * sdk: release v2.121.0-beta.11 * sdk: round tick/step size for getVammL2Generateor (#1612) * sdk: round tick/step size for etVammL2Generateor * use standard functions, include in all fcns * fix const declare, rm whitespace * fix posdir sign * sdk: release v2.121.0-beta.12 * sdk: release v2.121.0-beta.13 * sdk: constants market-index-45-46 (#1618) * sdk: release v2.121.0-beta.14 * robustness check for indicative quotes sender (#1621) * robustness check for indicative quotes sender * delete quote from market index of bad quote * sdk: release v2.121.0-beta.15 * Added launchTs for ZEUS, zBTC * sdk: release v2.121.0-beta.16 * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622) * sdk: release v2.121.0-beta.17 * sdk: fix vamm l2 generator base swapped (#1623) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * sdk: release v2.121.0-beta.18 * sdk: revert vamm l2 gen (#1624) * Revert "sdk: fix vamm l2 generator base swapped (#1623)" This reverts commit 56bc78d70e82cb35a90f12f73162bffb640cb655. * Revert "sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622)" This reverts commit e49cfd554cc44cd8d7770184f02f6ddb0bfc92f1. * Revert "sdk: round tick/step size for getVammL2Generateor (#1612)" This reverts commit f932a4ea2afcae314e406b7c7ee35e55b36043ad. * sdk: release v2.121.0-beta.19 * sdk: show protected-asset have zero-borrow-limit (#1603) * sdk: show protected-asset have zero-borrow-limit * rm unused AssetTier import * sdk: release v2.121.0-beta.20 * sdk: market-constants-index-74 (#1629) * sdk: release v2.121.0-beta.21 * program: use saturating_sub for number_of_users (#1616) * program: use saturating_sub for number_of_users * update CHANGELOG.md * program: allow fixing hlm num users (#1630) * sdk: release v2.121.0-beta.22 * sdk: fix switchboard on demand client to use landed at * sdk: release v2.121.0-beta.23 * sdk: spot-market-poolid-4 constants (#1631) * sdk: release v2.121.0-beta.24 * fix high lev mode liq price (#1632) * sdk: release v2.121.0-beta.25 * replace deprecated solana install scripts (#1634) * sdk: release v2.121.0-beta.26 * refactor(sdk): use ReturnType for Timeout types (#1637) * sdk: release v2.121.0-beta.27 * auction price sdk fix * sdk: release v2.121.0-beta.28 * program: multi piecewise interest rate curve (#1560) * program: multi-piecewise-interest-rate-curve * update tests * widen out borrow limits/healthy util check * add break, use array of array for borrow slope segments * program: fix cargo test * sdk: add segmented IR curve to interest rate calc * clean up unusded var, make interest rate segment logic a const * incorp efficiency feedback points * test: add sol realistic market example * cargo fmt -- * CHANGELOG --------- Co-authored-by: Chris Heaney * sdk: release v2.121.0-beta.29 * program: allow hot admin to update market fuel params (#1640) * v2.121.0 * sdk: release v2.122.0-beta.0 * sdk: fix nullish coalescing * sdk: release v2.122.0-beta.1 * program: add logging for wrong perp market mutability * sdk: check free collateral change in maxTradeSizeUsdcForPerp (#1645) * sdk: check free collateral change in maxTradeSizeUsdcForPerp * update changelog * sdk: release v2.122.0-beta.2 * refactor(sdk): emit newSlot event on initial subscribe call (#1646) * sdk: release v2.122.0-beta.3 * sdk: spot-market-constants-pool-id-2 (#1647) * sdk: release v2.122.0-beta.4 * sdk: add-spot-market-index-52-constants (#1649) * sdk: release v2.122.0-beta.5 * program: add existing position fields to order records (#1614) * program: add quote entry amount to order records * fix cargo fmt and test * more reusable code * more reusable code * add another comment * fix math * account for pos flip * fix typo * missed commit * more fixes * align naming * fix typo * CHANGELOG * program: check limit price after applying buffer in trigger limit ord… (#1648) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * fix tests * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: fix cargo tests * program: check limit price when setting auction for limit order (#1650) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * program: check limit price when setting limit auction params * cargo fmt -- * fix CHANGELOG * tests: updates switchboardTxCus.ts * program: try to fix iteration for max order size (#1651) * Revert "program: try to fix iteration for max order size (#1651)" This reverts commit 3f0eab39ed23fa4a9c41cbab9af793c60b50a239. * disable debug logging in bankrun tests * v2.122.0 * sdk: release v2.123.0-beta.0 * sdk: constants-spot-market-index-53 (#1655) * sdk: release v2.123.0-beta.1 * sdk: idl for new existing position order action records * fix: protocol test prettier fix * make ci lut checks not shit * sdk: release v2.123.0-beta.2 * sdk: fix vamm l2 generator base swapped and add new top of book (#1626) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * use proper quoteAmount with baseSwap for top of book orders * clean up console.log * sdk: getVammL2Generator reduce loc (#1628) * sdk: getVammL2Generator-reduce-loc * add MAJORS_TOP_OF_BOOK_QUOTE_AMOUNTS * add marketindex check topOfBookAmounts * yarn lint/prettier * sdk: release v2.123.0-beta.3 * program: allow all limit orders to go through swift (#1661) * program: allow all limit orders to go through swift * add anchor test * CHANGELOG * sdk: add optional initSwiftAccount on existing account deposits (#1660) * sdk: release v2.123.0-beta.4 * program: add taker_speed_bump_override and amm_spread_adjustment * Revert "program: add taker_speed_bump_override and amm_spread_adjustment" This reverts commit 1e19b7e7a6c5cecebdbfb3a9e224a0d4471ba6d2. * program: tests-fee-adjustment-neg-100 (#1656) * program: tests-fee-adjustment-neg-100 * add HLM field to test * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: simplify user can skip duration (#1668) * program: simplify user can skip duration * update context * CHANGELOG * fix test * fix pmm tests --------- Co-authored-by: Chris Heaney * program: add taker_speed_bump_override and amm_spread_adjustment (#1665) * program: add taker_speed_bump_override and amm_spread_adjustment * add admin client * cargo test * add impl for amm_spread_adjustment * ensure no overflows * CHANGELOG * cargo fmt -- * sdk types * prettify --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: update-amm-spread-and-availability-constraints (#1663) * program: update-amm-spread-and-availability-constraints * fix cargo tests * program: use saturating mul for amm spread adj * nour/indic-quotes-sender-v2 (#1667) * nour/indic-quotes-sender-v2 * prettify * pass margin category into calculateEntriesEffectOnFreeCollateral (#1669) * fix cargo test * tests: fix oracle guardrail test * sdk: update idl * yarn prettify:fix * tests: fix a few more place and make tests * prettify fix * whitespace readme change * sdk: release v2.123.0-beta.5 * v2.123.0 * sdk: release v2.124.0-beta.0 * v2.123.0-1 * sdk: calculateVolSpreadBN-sync (#1671) * sdk: release v2.124.0-beta.1 * sdk: calculate-spread-bn-add-amm-spread-adjustment (#1672) * sdk: calculate-spread-bn-add-amm-spread-adjustment * corect sign * add math max 1 * prettify * sdk: release v2.124.0-beta.2 * sdk: correct calculateVolSpreadBN reversion * sdk: release v2.124.0-beta.3 * sdk: add getTriggerAuctionStartPrice (#1654) * sdk: add getTriggerAuctionStartPrice * updates * precisions * remove startBuffer param --------- Co-authored-by: Chris Heaney * sdk: release v2.124.0-beta.4 * feat: customized cadence account loader (#1666) * feat: customized cadence account loader bby * feat: method to read account cadence on custom cadence account loader * feat: PR feedback on customized loader cleaup code and better naming * fix: lint and prettify * feat: more efficient rpc polling on custom polling intervals * feat: custom cadence acct loader override load * chore: prettify * sdk: release v2.124.0-beta.5 * sdk: sync-user-trade-tier-calcs (#1673) * sdk: sync-user-trade-tier-calcs * prettify --------- Co-authored-by: Nick Caradonna * sdk: release v2.124.0-beta.6 * sdk: add new admin client fn * Revert "sdk: add new admin client fn" This reverts commit c7a4f0b174858048bd379f2f2bb0e63595949921. * sdk: release v2.124.0-beta.7 * refactor(ui): add callback logic, fix polling frequency update * sdk: release v2.124.0-beta.8 * program: less order param sanitization for long tail perps (#1680) * program: allow-auction-start-buffer-on-tail-mkt * fix test * cargo fmt -- * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * Wphan/custom coder (#1682) * sdk: allow custom coder * remove unused accounts coder * linter * move customCoder into sdk, lint * update test helpers * update testhelpers.ts * sdk: release v2.124.0-beta.9 * update sdk exports * sdk: release v2.124.0-beta.10 * sdk: safer-calculate-spread-reserve-math (#1681) * sdk: release v2.124.0-beta.11 * update getMaxLeverageForPerp to use usdc logic (#1678) * sdk: release v2.124.0-beta.12 * program: override for oracle delay (#1679) * programy: override for oracle delay * update impl * switch to i8 * CHANGELOG * program: programmatic rebalance between protocol owned if holdings (#1653) * program: if swap * program: add initial config * add update * more * moar * moar * moar * program: update how swap epoch works * add test * add an invariant * cargo fmt -- * add transfer to rev pool * add mint validation * cargo fmt -- * track in amount between tranfsers * add to ci tests * separate key * program: always transfer max amount to rev pool * CHANGELOG * sdk: release v2.124.0-beta.13 * sdk: improve-aclient-accounts-logic (#1684) * sdk: release v2.124.0-beta.14 * program: improve-amm-spread-validates (#1685) * program: let hot wallet update amm jit intensity * sdk: hot wallet can update amm jit intensity * program: hot wallet can update curve intensity * program: fix build * sdk: update idl * sdk: release v2.124.0-beta.15 * v2.124.0 * sdk: release v2.125.0-beta.0 * program: three-point-std-estimator (#1686) * program: three-point-std-estimator * update tests and add sdk * update changelog * sdk: add-updatePerpMarketOracleSlotDelayOverride (#1691) * sdk: release v2.125.0-beta.1 * program: add-amm-inventory-spread-adjustment-param (#1690) * program: add-amm-inventory-spread-adjustment-param * cargo fmt -- * update sdk * prettier * fix syntax { --------- Co-authored-by: Chris Heaney * program: max-apr-rev-settle-by-spot-market (#1692) * program: max-apr-rev-settle-by-spot-market * update max * default to u128 to avoid casts * changelog * sdk: release v2.125.0-beta.2 * program: better account for imf in calculate_max_perp_order_size (#1693) * program: better account for imf in calculate_max_perp_order_size * CHANGELOG * v2.125.0 * sdk: release v2.126.0-beta.0 * sdk: only count taker fee in calculateEntriesEffectOnFreeCollateral for maintenance (#1694) * sdk: release v2.126.0-beta.1 * Separate getAddInsuranceFundStakeIxs (#1695) * sdk: release v2.126.0-beta.2 * idl: amm-inv-adj-latest-idl (#1697) * sdk: release v2.126.0-beta.3 * sdk: spot-market-index-54 constants (#1696) * sdk: release v2.126.0-beta.4 * sdk: update spot market index 54 pythlazer id * sdk: release v2.126.0-beta.5 * Update spotMarkets.ts * sdk: release v2.126.0-beta.6 * prettify --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: Nour Alharithi Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: lil perp Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * slot staleness checks (#1705) * slot staleness checks * update aum ix to use constituent oracles * Nour/derivative constituent testing (#1708) * slot staleness checks * update aum ix to use constituent oracles * constituent test works when adjusting derivative index * constituent depeg kill switch works * works with multiple derivatives on the same parent * remove incorrect usage of nav * fix adminClient and tests * Nour/fee grid search testing (#1714) * grid search * grid search swap test * Nour/address comments (#1715) * low hanging fruit comments * remove pda checks and store lp pool on zero copy accounts * parameterize depeg threshold * make description in lp pool event * update idl for event change * add swap fee unit tests (#1713) * add swap fee unit tests * remove linear inventory fee component * Nour/settle accounting (#1723) * fixing the main settle test and settle function * all current tests pass * update msg occurrences * dont update lp quote owed unless collateralized * Nour/settle testing (#1725) * refactor settle pnl to modularize and add tests * more cargo tests * prettify * Nour/address more comments (#1726) * use oracle staleness threshold for staleness * add spot market vault invariant * refactor update_aum, add unit tests (#1727) * refactor update_aum, add unit tests * add constituent target base tests * update doc * Nour/parameterize dlp (#1731) * add validates and test for withdraw limit * settlement max * update idl * merge conflicts * fixes * update idl * bug fixes * mostly sdk fixes * bug fixes * bug fix and deploy script * program: new amm oracle (#1738) * zero unused amm fields * cargo fmt * bare bones ix * minimal anchor mm oracle impl * update test file * only do admin validate when not anchor test * updates * generalize native entry * fix weird function name chop off * make it compile for --feature cpi (#1748) Co-authored-by: jordy25519 * more efficeint clock and state bit flags check * vamm uses mm oracle (#1747) * add offset * working tests * refactor to use MM oracle as its own type * remove weird preface * sdk updates * bankrun tests all pass * fix test * changes and fixes * widen confidence if mm oracle too diff * sdk side for confidence adjust * changelog * fix lint * fix cargo tests * address comments * add conf check * remove anchor ix and cache oracle confidence * only state admin can reenable mm oracle kill switch * cargo fmt --------- Co-authored-by: jordy25519 * fix tests (#1764) * Nour/move ixs around (#1766) * move around ixs * remove message * add devnet oracle crank wallet * refactored mm oracle * sdk changes + cargo fmt * fix tests * validate price bands with fill fix * normalize fill within price bands * add sdk warning * updated type * undefined guard so anchor tests pass * accept vec for update amm and view amm * adjust test to work with new price bands * Revert "adjust test to work with new price bands" This reverts commit ee40ac8799fa2f6222ea7d0e9b3e07014346a699. * remove price bands logic * add zero ix for mm oracle for reset * add new drift client ix grouping * v1 safety improvements * isolate funding from MM oracle * add cargo tests for amm availability * change oracle validity log bool to enum * address comment * make validate fill direction agnostic * fix liquidate borrow for perp pnl test * fix tests and address comments * add RevenueShare and RevenueShareEscrow accounts an init ixs * fix multiple array zc account, and handling different message types in place_signed_msg_taker_order * decoding error * commit constituent map to barrel file * add lp fields to perp market account * recording orders in RevenueShareEscrow workin * rearrange perp market struct for lp fields * cancel and fill orders * idl * fix sdk build * fix math * bug fix for notional position tracking * update RevenueShareOrder bitflags, store builder_idx instead of pubkey * view function * merge RevenueShareOrders on add * fee view functions * max aum + whitelist check and removing get_mint_redeem_fee for now * add wsol support for add liquidity * fix sdk and typing bugs * update lp pool params ix * admin override cache and disable settle functions * remove builder accounts from cancel ixs, wip settle impl * dont fail settlpnl if no builder users provided * devnet swap working * finish settle, rename RevenueShare->Builder, RevenueShareEscrow->BuilderEscrow * add more bankrun tests, clean up * clean up, fix tests * why test fail * dlp taker discovered bug fixes and sdk changes * add subaccountid to BuilderOrder * reduce diff * refactor last settle ts to last settle slot * add referrals * add test can fill settle user with no builderescrow * add referral builder feature flag and referral migration method * fix cargo tests, try fix bankrun test timing issue * Nour/settle pnl fix (#1817) * settle perp to lp pool bug fixes * update bankrun test to not use admin fee pool deposit * fix tests using update spot market balances too * add log msgs for withdraw and fix casting bug * add SignedMsgOrderParamsMessageV2 * check in for z (#1823) * feat: option for custom oracle ws subscriber * fix: pass custom oracle ws sub option in dc constructor * sdk: add spot-market-index-57 to constants (#1815) * sdk: release v2.134.0-beta.2 * lazer oracle migration (#1813) * lazer oracle migration * spot markets too * sdk: release v2.134.0-beta.3 * sdk: release v2.134.0-beta.4 * program: settle pnl invariants (#1812) * program: settle pnl invariants * add test * fix lint * lints * add msg * CHANGELOG * cargo fmt -- * program: add_update_perp_pnl_pool (#1810) * program: add_update_perp_pnl_pool * test * CHANGELOG * sdk: release v2.134.0-beta.5 * program: update-mark-twap-integer-bias (#1783) * program: update-mark-twap-integer-bias * changelog update * program: update-fee-tier-determine-fix5 (#1800) * program: update-fee-tier-determine-fix5 * update changelog * program: update-mark-twap-crank-use-5min-basis (#1769) * program: update-mark-twap-crank-use-5min-basis * changelog * program: update-min-margin-const-limit (#1802) * program: update-min-margin-const-limit * add CHANGELOG.md * sdk: release v2.134.0-beta.6 * program: rm-burn-lp-shares-invariant (#1816) * program: rm-burn-lp-shares-invariant * update changelog * fix test and cargo fmt * fix anchor tests * yarn prettify:fix * reenable settle_pnl mode test * v2.134.0 * sdk: release v2.135.0-beta.0 * Merge pull request #1820 from drift-labs/chester/fix-zod * sdk: release v2.135.0-beta.1 * mm oracle sdk change (#1806) * mm oracle sdk change * better conditional typing * DLOB bug fix * updated idl * rm getAmmBidAskPrice * sdk: release v2.135.0-beta.2 * sdk: fix isHighLeverageMode * sdk: release v2.135.0-beta.3 * refactor(sdk): add update delegate ix method, ovrride authority for settle multiple pnl (#1822) * check in for z * more logging changes * mm oracle sdk additions (#1824) * strict typing for more MM oracle contact points * add comments to auction.ts * prettify * sdk: release v2.135.0-beta.4 * init constituent bug fix and type change * add in invariant to be within 1 bp of balance before after * add strict typing for getPrice and new auction trigger function (#1826) * add strict typing for getPrice and new auction trigger function * refactor getTriggerAuctionStartAndExecutionPrice * sdk: release v2.135.0-beta.5 * update tests and enforce atomic settles for withdraw * add failing withdraw test * withdraw fix * bring diff in validate back to 1 * make lp pool test fail * better failed test * only check after < before, do to spot precision limits * add balance check to be < 1 cent --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: LukasDeco Co-authored-by: lil perp Co-authored-by: wphan Co-authored-by: Chester Sim * zero pad swift messages to make backwards compatible * PR feedback * add price for lp validates (#1833) * update tests/placeAndMakeSignedMsgBankrun.ts to handle client side errors * add missing token account reloads and syncs * add disabled lp pool swaps by default * refactor account logic for borrows * remove double fee count, update tests to check filled position and quote amounts fda * more extensive aum logging * rename Builder -> RevenueShare * add test check accumulated builder/ref fees * fix settle multiple pnl accounts, test ref rewards in multiple markets * express builder fees in tenth of bps * update referral migration params * PR feedback * add builder code feature gate * fix tests * add referral fields * run all tests * kickoff build * disable extra instructions, fix builder code feature flag selection * update driftclient * Revert recent builder codes chain and merge (#1848) * Revert recent builder codes chain and merge * update driftclient * disable extra instructions, fix builder code feature flag selection * clean up account inclusion rules in settle pnl for builder codes * cargo fmt * PR comments, featureflag clean up * move authority check into get_revenue_share_escrow_account * clean up referrer eligibility check, support placeAndTake/Make referral fees * skip builder fee accrual on full escrow account, dont throw * add feature flag sdk fn * program: builder codes dont throw tx on missing acc * placeAndMake respect builder codes * ensure update userstats referrerstatus on migration * hold back OrderActionRecord idl changes * update CHANGELOG.md --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> Co-authored-by: moosecat Co-authored-by: Chris Heaney Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * sdk: release v2.140.0-beta.2 * v2.140.0 * sdk: release v2.141.0-beta.0 * feat: add margin ratio ix to open orders + swift prop (#1864) * feat: add margin ratio ix to open orders + swift prop * fix: bug with max lev available calculation * fix: bug with swift msg encoding + margin ratio * feat: re-add types for swift non-optional * rm: unneeded undefined check on swift maxMarginRation * allow enter HLM on position margin ratio update * fix margin ratio calc * updates * rm logs --------- Co-authored-by: Nick Caradonna * sdk: release v2.141.0-beta.1 --------- Co-authored-by: jordy25519 Co-authored-by: Jack Waller Co-authored-by: lil perp Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Co-authored-by: moosecat Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> --- CHANGELOG.md | 10 + Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- programs/drift/src/controller/liquidation.rs | 5 + programs/drift/src/controller/mod.rs | 1 + programs/drift/src/controller/orders.rs | 208 ++- .../src/controller/orders/amm_jit_tests.rs | 26 + .../src/controller/orders/amm_lp_jit_tests.rs | 20 + .../drift/src/controller/orders/fuel_tests.rs | 2 + programs/drift/src/controller/orders/tests.rs | 82 + .../drift/src/controller/revenue_share.rs | 197 ++ programs/drift/src/error.rs | 16 + programs/drift/src/instructions/admin.rs | 44 + programs/drift/src/instructions/keeper.rs | 221 ++- .../src/instructions/optional_accounts.rs | 42 +- programs/drift/src/instructions/user.rs | 287 +++ programs/drift/src/lib.rs | 49 + programs/drift/src/math/fees.rs | 26 + programs/drift/src/math/fees/tests.rs | 21 + programs/drift/src/state/events.rs | 28 +- programs/drift/src/state/mod.rs | 2 + programs/drift/src/state/order_params.rs | 4 + programs/drift/src/state/revenue_share.rs | 572 ++++++ programs/drift/src/state/revenue_share_map.rs | 209 +++ programs/drift/src/state/state.rs | 10 + programs/drift/src/state/user.rs | 18 + .../drift/src/validation/sig_verification.rs | 6 + .../src/validation/sig_verification/tests.rs | 63 + sdk/VERSION | 2 +- sdk/package.json | 2 +- sdk/src/addresses/pda.ts | 26 + sdk/src/adminClient.ts | 183 ++ sdk/src/constants/perpMarkets.ts | 13 + sdk/src/driftClient.ts | 670 ++++++- sdk/src/idl/drift.json | 613 ++++++- sdk/src/index.ts | 1 + sdk/src/math/builder.ts | 20 + sdk/src/math/orders.ts | 5 + sdk/src/math/state.ts | 8 + sdk/src/memcmp.ts | 11 + sdk/src/swift/swiftOrderSubscriber.ts | 15 +- sdk/src/types.ts | 54 + sdk/src/user.ts | 51 +- sdk/src/userMap/revenueShareEscrowMap.ts | 306 ++++ sdk/tests/dlob/helpers.ts | 1 + test-scripts/run-anchor-tests.sh | 1 + test-scripts/run-til-failure.sh | 13 + tests/builderCodes.ts | 1612 +++++++++++++++++ tests/lpPool.ts | 12 +- tests/lpPoolSwap.ts | 2 +- tests/placeAndMakeSignedMsgBankrun.ts | 2 +- tests/subaccounts.ts | 1 + tests/switchboardTxCus.ts | 2 +- tests/testHelpers.ts | 13 +- 54 files changed, 5689 insertions(+), 123 deletions(-) create mode 100644 programs/drift/src/controller/revenue_share.rs create mode 100644 programs/drift/src/state/revenue_share.rs create mode 100644 programs/drift/src/state/revenue_share_map.rs create mode 100644 sdk/src/math/builder.ts create mode 100644 sdk/src/userMap/revenueShareEscrowMap.ts create mode 100644 test-scripts/run-til-failure.sh create mode 100644 tests/builderCodes.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f206720572..6ffe191d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +## [2.140.0] - 2025-09-29 + +### Features + +- program: builder codes ([#1805](https://github.com/drift-labs/protocol-v2/pull/1805)) + +### Fixes + +### Breaking + ## [2.139.0] - 2025-09-25 ### Features diff --git a/Cargo.lock b/Cargo.lock index cc1c58e9fa..1f28352549 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.139.0" +version = "2.140.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 6f37757604..c944d586f2 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.139.0" +version = "2.140.0" description = "Created with Anchor" edition = "2018" diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 1fd2f548dc..9503f24966 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -675,6 +675,8 @@ pub fn liquidate_perp( maker_existing_quote_entry_amount: maker_existing_quote_entry_amount, maker_existing_base_asset_amount: maker_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit!(fill_record); @@ -1038,6 +1040,7 @@ pub fn liquidate_perp_with_fill( clock, order_params, PlaceOrderOptions::default().explanation(OrderActionExplanation::Liquidation), + &mut None, )?; drop(user); @@ -1058,6 +1061,8 @@ pub fn liquidate_perp_with_fill( None, clock, FillMode::Liquidation, + &mut None, + false, )?; let mut user = load_mut!(user_loader)?; diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index 1565eb1174..5ebdb9772a 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -7,6 +7,7 @@ pub mod pda; pub mod pnl; pub mod position; pub mod repeg; +pub mod revenue_share; pub mod spot_balance; pub mod spot_position; pub mod token; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index e623000af3..57431d991c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -5,6 +5,9 @@ use std::u64; use crate::msg; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; +use crate::state::revenue_share::{ + RevenueShareEscrowZeroCopyMut, RevenueShareOrder, RevenueShareOrderBitFlag, +}; use anchor_lang::prelude::*; use crate::controller::funding::settle_funding_payment; @@ -103,6 +106,7 @@ pub fn place_perp_order( clock: &Clock, mut params: OrderParams, mut options: PlaceOrderOptions, + rev_share_order: &mut Option<&mut RevenueShareOrder>, ) -> DriftResult { let now = clock.unix_timestamp; let slot: u64 = clock.slot; @@ -298,6 +302,10 @@ pub fn place_perp_order( OrderBitFlag::NewTriggerReduceOnly, ); + if rev_share_order.is_some() { + bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::HasBuilder); + } + let new_order = Order { status: OrderStatus::Open, order_type: params.order_type, @@ -438,6 +446,8 @@ pub fn place_perp_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -718,6 +728,8 @@ pub fn cancel_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; } @@ -844,6 +856,7 @@ pub fn modify_order( clock, order_params, PlaceOrderOptions::default(), + &mut None, )?; } else { place_spot_order( @@ -968,6 +981,8 @@ pub fn fill_perp_order( jit_maker_order_id: Option, clock: &Clock, fill_mode: FillMode, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let now = clock.unix_timestamp; let slot = clock.slot; @@ -1304,6 +1319,8 @@ pub fn fill_perp_order( amm_availability, fill_mode, oracle_stale_for_margin, + rev_share_escrow, + builder_referral_feature_enabled, )?; if base_asset_amount != 0 { @@ -1714,6 +1731,37 @@ fn get_referrer_info( Ok(Some((referrer_authority_key, referrer_user_key))) } +#[inline(always)] +fn get_builder_escrow_info( + escrow_opt: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + sub_account_id: u16, + order_id: u32, + market_index: u16, + builder_referral_feature_enabled: bool, +) -> (Option, Option, Option, Option) { + if let Some(escrow) = escrow_opt { + let builder_order_idx = escrow.find_order_index(sub_account_id, order_id); + let referrer_builder_order_idx = if builder_referral_feature_enabled { + escrow.find_or_create_referral_index(market_index) + } else { + None + }; + + let builder_order = builder_order_idx.and_then(|idx| escrow.get_order(idx).ok()); + let builder_order_fee_bps = builder_order.map(|order| order.fee_tenth_bps); + let builder_idx = builder_order.map(|order| order.builder_idx); + + ( + builder_order_idx, + referrer_builder_order_idx, + builder_order_fee_bps, + builder_idx, + ) + } else { + (None, None, None, None) + } +} + fn fulfill_perp_order( user: &mut User, user_order_index: usize, @@ -1738,6 +1786,8 @@ fn fulfill_perp_order( amm_availability: AMMAvailability, fill_mode: FillMode, oracle_stale_for_margin: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let market_index = user.orders[user_order_index].market_index; @@ -1836,6 +1886,8 @@ fn fulfill_perp_order( None, *maker_price, fill_mode.is_liquidation(), + rev_share_escrow, + builder_referral_feature_enabled, )?; (fill_base_asset_amount, fill_quote_asset_amount) @@ -1880,6 +1932,8 @@ fn fulfill_perp_order( fee_structure, oracle_map, fill_mode.is_liquidation(), + rev_share_escrow, + builder_referral_feature_enabled, )?; if maker_fill_base_asset_amount != 0 { @@ -2123,6 +2177,8 @@ pub fn fulfill_perp_order_with_amm( override_base_asset_amount: Option, override_fill_price: Option, is_liquidation: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let position_index = get_position_index(&user.perp_positions, market.market_index)?; let existing_base_asset_amount = user.perp_positions[position_index].base_asset_amount; @@ -2180,8 +2236,13 @@ pub fn fulfill_perp_order_with_amm( return Ok((0, 0)); } - let (order_post_only, order_slot, order_direction) = - get_struct_values!(user.orders[order_index], post_only, slot, direction); + let (order_post_only, order_slot, order_direction, order_id) = get_struct_values!( + user.orders[order_index], + post_only, + slot, + direction, + order_id + ); validation::perp_market::validate_amm_account_for_fill(&market.amm, order_direction)?; @@ -2223,10 +2284,24 @@ pub fn fulfill_perp_order_with_amm( )?; } - let reward_referrer = can_reward_user_with_perp_pnl(referrer, market.market_index); + let reward_referrer = can_reward_user_with_referral_reward( + referrer, + market.market_index, + rev_share_escrow, + builder_referral_feature_enabled, + ); let reward_filler = can_reward_user_with_perp_pnl(filler, market.market_index) || can_reward_user_with_perp_pnl(maker, market.market_index); + let (builder_order_idx, referrer_builder_order_idx, builder_order_fee_bps, builder_idx) = + get_builder_escrow_info( + rev_share_escrow, + user.sub_account_id, + order_id, + market.market_index, + builder_referral_feature_enabled, + ); + let FillFees { user_fee, fee_to_market, @@ -2235,6 +2310,7 @@ pub fn fulfill_perp_order_with_amm( referrer_reward, fee_to_market_for_lp: _fee_to_market_for_lp, maker_rebate, + builder_fee: builder_fee_option, } = fees::calculate_fee_for_fulfillment_with_amm( user_stats, quote_asset_amount, @@ -2248,8 +2324,20 @@ pub fn fulfill_perp_order_with_amm( order_post_only, market.fee_adjustment, user.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order_fee_bps, )?; + let builder_fee = builder_fee_option.unwrap_or(0); + + if builder_fee != 0 { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_mut()) { + let mut order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } + // Increment the protocol's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; market.amm.total_exchange_fee = market.amm.total_exchange_fee.safe_add(user_fee.cast()?)?; @@ -2271,7 +2359,12 @@ pub fn fulfill_perp_order_with_amm( user_stats.increment_total_rebate(maker_rebate)?; user_stats.increment_total_referee_discount(referee_discount)?; - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let (Some(idx), Some(escrow)) = (referrer_builder_order_idx, rev_share_escrow.as_mut()) { + let order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2282,11 +2375,11 @@ pub fn fulfill_perp_order_with_amm( let position_index = get_position_index(&user.perp_positions, market.market_index)?; - if user_fee != 0 { + if user_fee != 0 || builder_fee != 0 { controller::position::update_quote_asset_and_break_even_amount( &mut user.perp_positions[position_index], market, - -user_fee.cast()?, + -(user_fee.safe_add(builder_fee)?).cast()?, )?; } @@ -2326,11 +2419,18 @@ pub fn fulfill_perp_order_with_amm( )?; } - update_order_after_fill( + let is_filled = update_order_after_fill( &mut user.orders[order_index], base_asset_amount, quote_asset_amount, )?; + if is_filled { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_mut()) { + let _ = escrow + .get_order_mut(idx) + .map(|order| order.add_bit_flag(RevenueShareOrderBitFlag::Completed)); + } + } decrease_open_bids_and_asks( &mut user.perp_positions[position_index], @@ -2391,7 +2491,7 @@ pub fn fulfill_perp_order_with_amm( Some(filler_reward), Some(base_asset_amount), Some(quote_asset_amount), - Some(user_fee), + Some(user_fee.safe_add(builder_fee)?), if maker_rebate != 0 { Some(maker_rebate) } else { @@ -2411,6 +2511,8 @@ pub fn fulfill_perp_order_with_amm( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_idx, + builder_fee_option, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2479,6 +2581,8 @@ pub fn fulfill_perp_order_with_match( fee_structure: &FeeStructure, oracle_map: &mut OracleMap, is_liquidation: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2591,6 +2695,8 @@ pub fn fulfill_perp_order_with_match( Some(jit_base_asset_amount), Some(maker_price), // match the makers price is_liquidation, + rev_share_escrow, + builder_referral_feature_enabled, )?; total_base_asset_amount = base_asset_amount_filled_by_amm; @@ -2683,9 +2789,23 @@ pub fn fulfill_perp_order_with_match( taker_stats.update_taker_volume_30d(market.fuel_boost_taker, quote_asset_amount, now)?; - let reward_referrer = can_reward_user_with_perp_pnl(referrer, market.market_index); + let reward_referrer = can_reward_user_with_referral_reward( + referrer, + market.market_index, + rev_share_escrow, + builder_referral_feature_enabled, + ); let reward_filler = can_reward_user_with_perp_pnl(filler, market.market_index); + let (builder_order_idx, referrer_builder_order_idx, builder_order_fee_bps, builder_idx) = + get_builder_escrow_info( + rev_share_escrow, + taker.sub_account_id, + taker.orders[taker_order_index].order_id, + market.market_index, + builder_referral_feature_enabled, + ); + let filler_multiplier = if reward_filler { calculate_filler_multiplier_for_matched_orders(maker_price, maker_direction, oracle_price)? } else { @@ -2699,6 +2819,7 @@ pub fn fulfill_perp_order_with_match( filler_reward, referrer_reward, referee_discount, + builder_fee: builder_fee_option, .. } = fees::calculate_fee_for_fulfillment_with_match( taker_stats, @@ -2713,7 +2834,18 @@ pub fn fulfill_perp_order_with_match( &MarketType::Perp, market.fee_adjustment, taker.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order_fee_bps, )?; + let builder_fee = builder_fee_option.unwrap_or(0); + + if builder_fee != 0 { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { + let mut order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } // Increment the markets house's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; @@ -2733,7 +2865,7 @@ pub fn fulfill_perp_order_with_match( controller::position::update_quote_asset_and_break_even_amount( &mut taker.perp_positions[taker_position_index], market, - -taker_fee.cast()?, + -(taker_fee.safe_add(builder_fee)?).cast()?, )?; taker_stats.increment_total_fees(taker_fee)?; @@ -2772,7 +2904,13 @@ pub fn fulfill_perp_order_with_match( filler.update_last_active_slot(slot); } - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let (Some(idx), Some(escrow)) = (referrer_builder_order_idx, rev_share_escrow.as_deref_mut()) + { + let mut order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2781,12 +2919,20 @@ pub fn fulfill_perp_order_with_match( } } - update_order_after_fill( + let is_filled = update_order_after_fill( &mut taker.orders[taker_order_index], base_asset_amount_fulfilled_by_maker, quote_asset_amount, )?; + if is_filled { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { + escrow + .get_order_mut(idx)? + .add_bit_flag(RevenueShareOrderBitFlag::Completed); + } + } + decrease_open_bids_and_asks( &mut taker.perp_positions[taker_position_index], &taker.orders[taker_order_index].direction, @@ -2841,7 +2987,7 @@ pub fn fulfill_perp_order_with_match( Some(filler_reward), Some(base_asset_amount_fulfilled_by_maker), Some(quote_asset_amount), - Some(taker_fee), + Some(taker_fee.safe_add(builder_fee)?), Some(maker_rebate), Some(referrer_reward), None, @@ -2857,6 +3003,8 @@ pub fn fulfill_perp_order_with_match( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_idx, + builder_fee_option, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2885,18 +3033,19 @@ pub fn update_order_after_fill( order: &mut Order, base_asset_amount: u64, quote_asset_amount: u64, -) -> DriftResult { +) -> DriftResult { order.base_asset_amount_filled = order.base_asset_amount_filled.safe_add(base_asset_amount)?; order.quote_asset_amount_filled = order .quote_asset_amount_filled .safe_add(quote_asset_amount)?; - if order.get_base_asset_amount_unfilled(None)? == 0 { + let is_filled = order.get_base_asset_amount_unfilled(None)? == 0; + if is_filled { order.status = OrderStatus::Filled; } - Ok(()) + Ok(is_filled) } #[allow(clippy::type_complexity)] @@ -3092,6 +3241,8 @@ pub fn trigger_order( None, None, Some(trigger_price), + None, + None, )?; emit!(order_action_record); @@ -3299,6 +3450,22 @@ pub fn can_reward_user_with_perp_pnl(user: &mut Option<&mut User>, market_index: } } +pub fn can_reward_user_with_referral_reward( + user: &mut Option<&mut User>, + market_index: u16, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, +) -> bool { + if builder_referral_feature_enabled { + if let Some(escrow) = rev_share_escrow { + return escrow.find_or_create_referral_index(market_index).is_some(); + } + false + } else { + can_reward_user_with_perp_pnl(user, market_index) + } +} + pub fn pay_keeper_flat_reward_for_perps( user: &mut User, filler: Option<&mut User>, @@ -3655,6 +3822,8 @@ pub fn place_spot_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -4730,6 +4899,7 @@ pub fn fulfill_spot_order_with_match( &MarketType::Spot, base_market.fee_adjustment, false, + None, )?; // Update taker state @@ -4897,6 +5067,8 @@ pub fn fulfill_spot_order_with_match( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5171,6 +5343,8 @@ pub fn fulfill_spot_order_with_external_market( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5375,6 +5549,8 @@ pub fn trigger_spot_order( None, None, Some(oracle_price.unsigned_abs()), + None, + None, )?; emit!(order_action_record); diff --git a/programs/drift/src/controller/orders/amm_jit_tests.rs b/programs/drift/src/controller/orders/amm_jit_tests.rs index 6d8ff77eef..c14fd58b62 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -300,6 +300,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -490,6 +492,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -688,6 +692,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -885,6 +891,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1084,6 +1092,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1292,6 +1302,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1498,6 +1510,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::Unavailable, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1698,6 +1712,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1886,6 +1902,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2086,6 +2104,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2337,6 +2357,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2621,6 +2643,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2850,6 +2874,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs index 550574a1c7..ae6666328e 100644 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs @@ -504,6 +504,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -706,6 +708,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -908,6 +912,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1119,6 +1125,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1322,6 +1330,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1513,6 +1523,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1716,6 +1728,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1967,6 +1981,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2251,6 +2267,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2483,6 +2501,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index f29b54addd..b4021e6b7b 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -245,6 +245,8 @@ pub mod fuel_scoring { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index b5e681f090..c8428f01d0 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -264,6 +264,8 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -373,6 +375,8 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -486,6 +490,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -609,6 +615,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -732,6 +740,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -855,6 +865,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -977,6 +989,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1066,6 +1080,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1156,6 +1172,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1246,6 +1264,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1336,6 +1356,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1446,6 +1468,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1561,6 +1585,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1681,6 +1707,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1802,6 +1830,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1947,6 +1977,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2067,6 +2099,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2197,6 +2231,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2348,6 +2384,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2497,6 +2535,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2647,6 +2687,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2778,6 +2820,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2908,6 +2952,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -3296,6 +3342,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3540,6 +3588,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3730,6 +3780,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3936,6 +3988,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4102,6 +4156,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4300,6 +4356,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ); assert!(result.is_ok()); @@ -4487,6 +4545,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); @@ -4627,6 +4687,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::Immediate, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4794,6 +4856,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4970,6 +5034,8 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -4995,6 +5061,8 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -5144,6 +5212,8 @@ pub mod fulfill_order { // slot, // false, // true, + // &mut None, + // false, // ) // .unwrap(); // @@ -5377,6 +5447,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -5621,6 +5693,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -5878,6 +5952,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6080,6 +6156,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6209,6 +6287,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6371,6 +6451,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ); assert_eq!(err, Err(ErrorCode::MaxOpenInterest)); diff --git a/programs/drift/src/controller/revenue_share.rs b/programs/drift/src/controller/revenue_share.rs new file mode 100644 index 0000000000..61681cd7a4 --- /dev/null +++ b/programs/drift/src/controller/revenue_share.rs @@ -0,0 +1,197 @@ +use anchor_lang::prelude::*; + +use crate::controller::spot_balance; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::events::{emit_stack, RevenueShareSettleRecord}; +use crate::state::perp_market_map::PerpMarketMap; +use crate::state::revenue_share::{RevenueShareEscrowZeroCopyMut, RevenueShareOrder}; +use crate::state::revenue_share_map::RevenueShareMap; +use crate::state::spot_market::SpotBalance; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::traits::Size; +use crate::state::user::MarketType; + +/// Runs through the user's RevenueShareEscrow account and sweeps any accrued fees to the corresponding +/// builders and referrer. +pub fn sweep_completed_revenue_share_for_market<'a>( + market_index: u16, + revenue_share_escrow: &mut RevenueShareEscrowZeroCopyMut, + perp_market_map: &PerpMarketMap<'a>, + spot_market_map: &SpotMarketMap<'a>, + revenue_share_map: &RevenueShareMap<'a>, + now_ts: i64, + builder_codes_feature_enabled: bool, + builder_referral_feature_enabled: bool, +) -> crate::error::DriftResult<()> { + let perp_market = &mut perp_market_map.get_ref_mut(&market_index)?; + let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; + + spot_balance::update_spot_market_cumulative_interest(quote_spot_market, None, now_ts)?; + + let orders_len = revenue_share_escrow.orders_len(); + for i in 0..orders_len { + let ( + is_completed, + is_referral_order, + order_market_type, + order_market_index, + fees_accrued, + builder_idx, + ) = { + let ord_ro = match revenue_share_escrow.get_order(i) { + Ok(o) => o, + Err(_) => { + continue; + } + }; + ( + ord_ro.is_completed(), + ord_ro.is_referral_order(), + ord_ro.market_type, + ord_ro.market_index, + ord_ro.fees_accrued, + ord_ro.builder_idx, + ) + }; + + if is_referral_order { + if fees_accrued == 0 + || !(order_market_type == MarketType::Perp && order_market_index == market_index) + { + continue; + } + } else if !(is_completed + && order_market_type == MarketType::Perp + && order_market_index == market_index + && fees_accrued > 0) + { + continue; + } + + let pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_spot_market, + perp_market.pnl_pool.balance_type(), + )?; + + // TODO: should we add buffer on pnl pool? + if pnl_pool_token_amount < fees_accrued as u128 { + msg!( + "market {} PNL pool has insufficient balance to sweep fees for builder. pnl_pool_token_amount: {}, fees_accrued: {}", + market_index, + pnl_pool_token_amount, + fees_accrued + ); + break; + } + + if is_referral_order { + if builder_referral_feature_enabled { + let referrer_authority = + if let Some(referrer_authority) = revenue_share_escrow.get_referrer() { + referrer_authority + } else { + continue; + }; + + let referrer_user = revenue_share_map.get_user_ref_mut(&referrer_authority); + let referrer_rev_share = + revenue_share_map.get_revenue_share_account_mut(&referrer_authority); + + if referrer_user.is_ok() && referrer_rev_share.is_ok() { + let mut referrer_user = referrer_user.unwrap(); + let mut referrer_rev_share = referrer_rev_share.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + referrer_user.get_quote_spot_position_mut(), + )?; + + referrer_rev_share.total_referrer_rewards = referrer_rev_share + .total_referrer_rewards + .safe_add(fees_accrued as u64)?; + + emit_stack::<_, { RevenueShareSettleRecord::SIZE }>( + RevenueShareSettleRecord { + ts: now_ts, + builder: None, + referrer: Some(referrer_authority), + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: referrer_rev_share + .total_referrer_rewards, + builder_total_builder_rewards: referrer_rev_share.total_builder_rewards, + builder_sub_account_id: referrer_user.sub_account_id, + }, + )?; + + // zero out the order + if let Ok(builder_order) = revenue_share_escrow.get_order_mut(i) { + builder_order.fees_accrued = 0; + } + } + } + } else if builder_codes_feature_enabled { + let builder_authority = match revenue_share_escrow + .get_approved_builder_mut(builder_idx) + .map(|builder| builder.authority) + { + Ok(auth) => auth, + Err(_) => { + msg!("failed to get approved_builder from escrow account"); + continue; + } + }; + + let builder_user = revenue_share_map.get_user_ref_mut(&builder_authority); + let builder_rev_share = + revenue_share_map.get_revenue_share_account_mut(&builder_authority); + + if builder_user.is_ok() && builder_rev_share.is_ok() { + let mut builder_user = builder_user.unwrap(); + let mut builder_revenue_share = builder_rev_share.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + builder_user.get_quote_spot_position_mut(), + )?; + + builder_revenue_share.total_builder_rewards = builder_revenue_share + .total_builder_rewards + .safe_add(fees_accrued as u64)?; + + emit_stack::<_, { RevenueShareSettleRecord::SIZE }>(RevenueShareSettleRecord { + ts: now_ts, + builder: Some(builder_authority), + referrer: None, + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: builder_revenue_share.total_referrer_rewards, + builder_total_builder_rewards: builder_revenue_share.total_builder_rewards, + builder_sub_account_id: builder_user.sub_account_id, + })?; + + // remove order + if let Ok(builder_order) = revenue_share_escrow.get_order_mut(i) { + *builder_order = RevenueShareOrder::default(); + } + } else { + msg!( + "Builder user or builder not found for builder authority: {}", + builder_authority + ); + } + } else { + msg!("Builder codes nor builder referral feature is not enabled"); + } + } + + Ok(()) +} diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index f8dcd300db..eaa8009e65 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -638,6 +638,22 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid RevenueShare resize")] + InvalidRevenueShareResize, + #[msg("Builder has been revoked")] + BuilderRevoked, + #[msg("Builder fee is greater than max fee bps")] + InvalidBuilderFee, + #[msg("RevenueShareEscrow authority mismatch")] + RevenueShareEscrowAuthorityMismatch, + #[msg("RevenueShareEscrow has too many active orders")] + RevenueShareEscrowOrdersAccountFull, + #[msg("Invalid RevenueShareAccount")] + InvalidRevenueShareAccount, + #[msg("Cannot revoke builder with open orders")] + CannotRevokeBuilderWithOpenOrders, + #[msg("Unable to load builder account")] + UnableToLoadRevenueShareAccount, #[msg("Invalid Constituent")] InvalidConstituent, #[msg("Invalid Amm Constituent Mapping argument")] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 374ab4a407..818b3a1419 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -5045,6 +5045,50 @@ pub fn handle_update_delegate_user_gov_token_insurance_stake( Ok(()) } +pub fn handle_update_feature_bit_flags_builder_codes( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can enable feature bit flags" + )?; + + msg!("Setting 3rd bit to 1, enabling builder codes"); + state.feature_bit_flags = state.feature_bit_flags | (FeatureBitFlags::BuilderCodes as u8); + } else { + msg!("Setting 3rd bit to 0, disabling builder codes"); + state.feature_bit_flags = state.feature_bit_flags & !(FeatureBitFlags::BuilderCodes as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_builder_referral( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can enable feature bit flags" + )?; + + msg!("Setting 4th bit to 1, enabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags | (FeatureBitFlags::BuilderReferral as u8); + } else { + msg!("Setting 4th bit to 0, disabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags & !(FeatureBitFlags::BuilderReferral as u8); + } + Ok(()) +} + pub fn handle_update_feature_bit_flags_settle_lp_pool( ctx: Context, enable: bool, diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 38c3d00f63..2b8fc96af6 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -26,6 +26,7 @@ use crate::get_then_update_id; use crate::ids::admin_hot_wallet; use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_program}; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; @@ -63,6 +64,10 @@ use crate::state::perp_market_map::{ get_market_set_for_spot_positions, get_market_set_for_user_positions, get_market_set_from_list, get_writable_perp_market_set, get_writable_perp_market_set_from_vec, MarketSet, PerpMarketMap, }; +use crate::state::revenue_share::RevenueShareEscrowZeroCopyMut; +use crate::state::revenue_share::RevenueShareOrder; +use crate::state::revenue_share::RevenueShareOrderBitFlag; +use crate::state::revenue_share_map::load_revenue_share_map; use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::signed_msg_user::{ SignedMsgOrderId, SignedMsgUserOrdersLoader, SignedMsgUserOrdersZeroCopyMut, @@ -140,7 +145,7 @@ fn fill_order<'c: 'info, 'info>( let clock = &Clock::get()?; let state = &ctx.accounts.state; - let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mut remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, @@ -156,6 +161,17 @@ fn fill_order<'c: 'info, 'info>( let (makers_and_referrer, makers_and_referrer_stats) = load_user_maps(remaining_accounts_iter, true)?; + let builder_codes_enabled = state.builder_codes_enabled(); + let builder_referral_enabled = state.builder_referral_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + &mut remaining_accounts_iter, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + controller::repeg::update_amm( market_index, &perp_market_map, @@ -179,6 +195,8 @@ fn fill_order<'c: 'info, 'info>( None, clock, FillMode::Fill, + &mut escrow.as_mut(), + builder_referral_enabled, )?; Ok(()) @@ -648,6 +666,12 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( let mut taker = load_mut!(ctx.accounts.user)?; let mut signed_msg_taker = ctx.accounts.signed_msg_user_orders.load_mut()?; + let escrow = if state.builder_codes_enabled() { + get_revenue_share_escrow_account(&mut remaining_accounts, &taker.authority)? + } else { + None + }; + place_signed_msg_taker_order( taker_key, &mut taker, @@ -658,6 +682,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, high_leverage_mode_config, + escrow, state, is_delegate_signer, )?; @@ -674,6 +699,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, high_leverage_mode_config: Option>, + escrow: Option>, state: &State, is_delegate_signer: bool, ) -> Result<()> { @@ -702,6 +728,43 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( is_delegate_signer, )?; + let mut escrow_zc: Option> = None; + let mut builder_fee_bps: Option = None; + if state.builder_codes_enabled() + && verified_message_and_signature.builder_idx.is_some() + && verified_message_and_signature + .builder_fee_tenth_bps + .is_some() + { + if let Some(mut escrow) = escrow { + let builder_idx = verified_message_and_signature.builder_idx.unwrap(); + let builder_fee = verified_message_and_signature + .builder_fee_tenth_bps + .unwrap(); + + validate!( + escrow.fixed.authority == taker.authority, + ErrorCode::InvalidUserAccount, + "RevenueShareEscrow account must be owned by taker", + )?; + + let builder = escrow.get_approved_builder_mut(builder_idx)?; + + if builder.is_revoked() { + return Err(ErrorCode::BuilderRevoked.into()); + } + + if builder_fee > builder.max_fee_tenth_bps { + return Err(ErrorCode::InvalidBuilderFee.into()); + } + + builder_fee_bps = Some(builder_fee); + escrow_zc = Some(escrow); + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } + if is_delegate_signer { validate!( verified_message_and_signature.delegate_signed_taker_pubkey == Some(taker_key), @@ -810,6 +873,33 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add stop loss order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -825,6 +915,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } @@ -847,6 +938,33 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add take profit order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -862,11 +980,39 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } signed_msg_order_id.order_id = taker_order_id_to_use; signed_msg_account.add_signed_msg_order_id(signed_msg_order_id)?; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -882,6 +1028,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( signed_msg_taker_order_slot: Some(order_slot), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; let order_params_hash = @@ -897,6 +1044,10 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ts: clock.unix_timestamp, }); + if let Some(ref mut escrow) = escrow_zc { + escrow.revoke_completed_orders(taker)?; + }; + Ok(()) } @@ -919,18 +1070,30 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( "user have pool_id 0" )?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); + let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set(market_index), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, Some(state.oracle_guard_rails), )?; + let (mut builder_escrow, maybe_rev_share_map) = + if state.builder_codes_enabled() || state.builder_referral_enabled() { + ( + get_revenue_share_escrow_account(&mut remaining_accounts, &user.authority)?, + load_revenue_share_map(&mut remaining_accounts).ok(), + ) + } else { + (None, None) + }; + let market_in_settlement = perp_market_map.get_ref(&market_index)?.status == MarketStatus::Settlement; @@ -975,6 +1138,26 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + if state.builder_codes_enabled() || state.builder_referral_enabled() { + if let Some(ref mut escrow) = builder_escrow { + escrow.revoke_completed_orders(user)?; + if let Some(ref builder_map) = maybe_rev_share_map { + controller::revenue_share::sweep_completed_revenue_share_for_market( + market_index, + escrow, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + state.builder_codes_enabled(), + state.builder_referral_enabled(), + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } + } + let spot_market = spot_market_map.get_quote_spot_market()?; validate_spot_market_vault_amount(&spot_market, ctx.accounts.spot_market_vault.amount)?; @@ -995,18 +1178,30 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( let user_key = ctx.accounts.user.key(); 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, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set_from_vec(&market_indexes), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, Some(state.oracle_guard_rails), )?; + let (mut builder_escrow, maybe_rev_share_map) = + if state.builder_codes_enabled() || state.builder_referral_enabled() { + ( + get_revenue_share_escrow_account(&mut remaining_accounts, &user.authority)?, + load_revenue_share_map(&mut remaining_accounts).ok(), + ) + } else { + (None, None) + }; + let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( user, &perp_market_map, @@ -1058,6 +1253,26 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + + if state.builder_codes_enabled() || state.builder_referral_enabled() { + if let Some(ref mut escrow) = builder_escrow { + escrow.revoke_completed_orders(user)?; + if let Some(ref builder_map) = maybe_rev_share_map { + controller::revenue_share::sweep_completed_revenue_share_for_market( + *market_index, + escrow, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + state.builder_codes_enabled(), + state.builder_referral_enabled(), + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } + } } let spot_market = spot_market_map.get_quote_spot_market()?; diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index 7abe5c33d2..c2365bf0ec 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -1,5 +1,8 @@ use crate::error::{DriftResult, ErrorCode}; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; +use crate::state::revenue_share::{ + RevenueShareEscrow, RevenueShareEscrowLoader, RevenueShareEscrowZeroCopyMut, +}; use std::cell::RefMut; use std::convert::TryFrom; @@ -17,7 +20,7 @@ use crate::state::traits::Size; use crate::state::user::{User, UserStats}; use crate::{validate, OracleSource}; use anchor_lang::accounts::account::Account; -use anchor_lang::prelude::{AccountInfo, Interface}; +use anchor_lang::prelude::{AccountInfo, Interface, Pubkey}; use anchor_lang::prelude::{AccountLoader, InterfaceAccount}; use anchor_lang::Discriminator; use anchor_spl::token::TokenAccount; @@ -273,3 +276,40 @@ pub fn get_high_leverage_mode_config<'a>( Ok(Some(high_leverage_mode_config)) } + +pub fn get_revenue_share_escrow_account<'a>( + account_info_iter: &mut Peekable>>, + expected_authority: &Pubkey, +) -> DriftResult>> { + let account_info = account_info_iter.peek(); + if account_info.is_none() { + return Ok(None); + } + + let account_info = account_info.safe_unwrap()?; + + // Check size and discriminator without borrowing + if account_info.data_len() < 80 { + return Ok(None); + } + + let discriminator: [u8; 8] = RevenueShareEscrow::discriminator(); + let borrowed_data = account_info.data.borrow(); + let account_discriminator = array_ref![&borrowed_data, 0, 8]; + if account_discriminator != &discriminator { + return Ok(None); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + drop(borrowed_data); + let escrow: RevenueShareEscrowZeroCopyMut<'a> = account_info.load_zc_mut()?; + + validate!( + escrow.fixed.authority == *expected_authority, + ErrorCode::RevenueShareEscrowAuthorityMismatch, + "invalid RevenueShareEscrow authority" + )?; + + Ok(Some(escrow)) +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 6bff0e6fb4..6d42cd4c4b 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -27,6 +27,7 @@ use crate::ids::{ serum_program, }; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{ get_referrer_and_referrer_stats, get_whitelist_token, load_maps, AccountMaps, }; @@ -79,6 +80,12 @@ use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::{get_writable_perp_market_set, MarketSet}; use crate::state::protected_maker_mode_config::ProtectedMakerModeConfig; +use crate::state::revenue_share::BuilderInfo; +use crate::state::revenue_share::RevenueShare; +use crate::state::revenue_share::RevenueShareEscrow; +use crate::state::revenue_share::RevenueShareOrder; +use crate::state::revenue_share::REVENUE_SHARE_ESCROW_PDA_SEED; +use crate::state::revenue_share::REVENUE_SHARE_PDA_SEED; use crate::state::signed_msg_user::SignedMsgOrderId; use crate::state::signed_msg_user::SignedMsgUserOrdersLoader; use crate::state::signed_msg_user::SignedMsgWsDelegates; @@ -493,6 +500,140 @@ pub fn handle_reset_fuel_season<'c: 'info, 'info>( Ok(()) } +pub fn handle_initialize_revenue_share<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShare<'info>>, +) -> Result<()> { + let mut revenue_share = ctx + .accounts + .revenue_share + .load_init() + .or(Err(ErrorCode::UnableToLoadAccountLoader))?; + revenue_share.authority = ctx.accounts.authority.key(); + revenue_share.total_referrer_rewards = 0; + revenue_share.total_builder_rewards = 0; + Ok(()) +} + +pub fn handle_initialize_revenue_share_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShareEscrow<'info>>, + num_orders: u16, +) -> Result<()> { + let escrow = &mut ctx.accounts.escrow; + escrow.authority = ctx.accounts.authority.key(); + escrow + .orders + .resize_with(num_orders as usize, RevenueShareOrder::default); + + let state = &mut ctx.accounts.state; + if state.builder_referral_enabled() { + let mut user_stats = ctx.accounts.user_stats.load_mut()?; + escrow.referrer = user_stats.referrer; + user_stats.update_builder_referral_status(); + } + + escrow.validate()?; + Ok(()) +} + +pub fn handle_migrate_referrer<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if !state.builder_referral_enabled() { + if state.admin != ctx.accounts.payer.key() + || ctx.accounts.payer.key() == admin_hot_wallet::id() + { + msg!("Only admin can migrate referrer until builder referral feature is enabled"); + return Err(anchor_lang::error::ErrorCode::ConstraintSigner.into()); + } + } + + let escrow = &mut ctx.accounts.escrow; + let mut user_stats = ctx.accounts.user_stats.load_mut()?; + escrow.referrer = user_stats.referrer; + user_stats.update_builder_referral_status(); + + escrow.validate()?; + Ok(()) +} + +pub fn handle_resize_revenue_share_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeRevenueShareEscrowOrders<'info>>, + num_orders: u16, +) -> Result<()> { + let escrow = &mut ctx.accounts.escrow; + validate!( + num_orders as usize >= escrow.orders.len(), + ErrorCode::InvalidRevenueShareResize, + "Invalid shrinking resize for revenue share escrow" + )?; + + escrow + .orders + .resize_with(num_orders as usize, RevenueShareOrder::default); + escrow.validate()?; + Ok(()) +} + +pub fn handle_change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_tenth_bps: u16, + add: bool, +) -> Result<()> { + let existing_builder_index = ctx + .accounts + .escrow + .approved_builders + .iter() + .position(|b| b.authority == builder); + if let Some(index) = existing_builder_index { + if add { + msg!( + "Updated builder: {} with max fee tenth bps: {} -> {}", + builder, + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps, + max_fee_tenth_bps + ); + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps = max_fee_tenth_bps; + } else { + if ctx + .accounts + .escrow + .orders + .iter() + .any(|o| (o.builder_idx == index as u8) && (!o.is_available())) + { + msg!("Builder has open orders, must cancel orders and settle_pnl before revoking"); + return Err(ErrorCode::CannotRevokeBuilderWithOpenOrders.into()); + } + msg!( + "Revoking builder: {}, max fee tenth bps: {} -> 0", + builder, + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps, + ); + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps = 0; + } + } else { + if add { + ctx.accounts.escrow.approved_builders.push(BuilderInfo { + authority: builder, + max_fee_tenth_bps, + ..BuilderInfo::default() + }); + msg!( + "Added builder: {} with max fee tenth bps: {}", + builder, + max_fee_tenth_bps + ); + } else { + msg!("Tried to revoke builder: {}, but it was not found", builder); + } + } + + Ok(()) +} + #[access_control( deposit_not_paused(&ctx.accounts.state) )] @@ -1889,6 +2030,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( maker_existing_quote_entry_amount: from_existing_quote_entry_amount, maker_existing_base_asset_amount: from_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit_stack::<_, { OrderActionRecord::SIZE }>(fill_record)?; @@ -1940,6 +2083,7 @@ pub fn handle_place_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; Ok(()) @@ -2242,6 +2386,7 @@ pub fn handle_place_orders<'c: 'info, 'info>( clock, *params, options, + &mut None, )?; } else { controller::orders::place_spot_order( @@ -2322,6 +2467,7 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( &clock, params, PlaceOrderOptions::default(), + &mut None, )?; drop(user); @@ -2329,6 +2475,14 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( let user = &mut ctx.accounts.user; let order_id = load!(user)?.get_last_order_id(); + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account(remaining_accounts_iter, &load!(user)?.authority)? + } else { + None + }; + let (base_asset_amount_filled, _) = controller::orders::fill_perp_order( order_id, &ctx.accounts.state, @@ -2347,6 +2501,8 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( is_immediate_or_cancel || optional_params.is_some(), auction_duration_percentage, ), + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_unfilled = load!(ctx.accounts.user)? @@ -2436,6 +2592,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2447,6 +2604,17 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.taker)?.authority, + )? + } else { + None + }; + controller::orders::fill_perp_order( taker_order_id, state, @@ -2462,6 +2630,8 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_exists = load!(ctx.accounts.user)? @@ -2537,6 +2707,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2548,6 +2719,17 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.taker)?.authority, + )? + } else { + None + }; + let taker_signed_msg_account = ctx.accounts.taker_signed_msg_user_orders.load()?; let taker_order_id = taker_signed_msg_account .iter() @@ -2570,6 +2752,8 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_exists = load!(ctx.accounts.user)? @@ -4586,3 +4770,106 @@ pub struct UpdateUserProtectedMakerMode<'info> { #[account(mut)] pub protected_maker_mode_config: AccountLoader<'info, ProtectedMakerModeConfig>, } + +#[derive(Accounts)] +#[instruction()] +pub struct InitializeRevenueShare<'info> { + #[account( + init, + seeds = [REVENUE_SHARE_PDA_SEED.as_ref(), authority.key().as_ref()], + space = RevenueShare::space(), + bump, + payer = payer + )] + pub revenue_share: AccountLoader<'info, RevenueShare>, + /// CHECK: The builder and/or referrer authority, beneficiary of builder/ref fees + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct InitializeRevenueShareEscrow<'info> { + #[account( + init, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + space = RevenueShareEscrow::space(num_orders as usize, 1), + bump, + payer = payer + )] + pub escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: AccountInfo<'info>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct MigrateReferrer<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + )] + pub escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: AccountInfo<'info>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + pub payer: Signer<'info>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct ResizeRevenueShareEscrowOrders<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + realloc = RevenueShareEscrow::space(num_orders as usize, escrow.approved_builders.len()), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub escrow: Box>, + /// CHECK: The owner of RevenueShareEscrow + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(builder: Pubkey, max_fee_tenth_bps: u16, add: bool)] +pub struct ChangeApprovedBuilder<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + // revoking a builder does not remove the slot to avoid unintended reuse + realloc = RevenueShareEscrow::space(escrow.orders.len(), if add { escrow.approved_builders.len() + 1 } else { escrow.approved_builders.len() }), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub escrow: Box>, + pub authority: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 0130e91ec7..53d05e25b7 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1871,6 +1871,55 @@ pub mod drift { handle_update_feature_bit_flags_median_trigger_price(ctx, enable) } + // pub fn update_feature_bit_flags_builder_referral( + // ctx: Context, + // enable: bool, + // ) -> Result<()> { + // handle_update_feature_bit_flags_builder_referral(ctx, enable) + // } + + pub fn update_feature_bit_flags_builder_codes( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_builder_codes(ctx, enable) + } + + pub fn initialize_revenue_share<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShare<'info>>, + ) -> Result<()> { + handle_initialize_revenue_share(ctx) + } + + pub fn initialize_revenue_share_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShareEscrow<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_initialize_revenue_share_escrow(ctx, num_orders) + } + + // pub fn migrate_referrer<'c: 'info, 'info>( + // ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, + // ) -> Result<()> { + // handle_migrate_referrer(ctx) + // } + + pub fn resize_revenue_share_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeRevenueShareEscrowOrders<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_resize_revenue_share_escrow_orders(ctx, num_orders) + } + + pub fn change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_bps: u16, + add: bool, + ) -> Result<()> { + handle_change_approved_builder(ctx, builder, max_fee_bps, add) + } + pub fn update_feature_bit_flags_settle_lp_pool( ctx: Context, enable: bool, diff --git a/programs/drift/src/math/fees.rs b/programs/drift/src/math/fees.rs index 3e23e718f3..4b358b071a 100644 --- a/programs/drift/src/math/fees.rs +++ b/programs/drift/src/math/fees.rs @@ -30,6 +30,7 @@ pub struct FillFees { pub filler_reward: u64, pub referrer_reward: u64, pub referee_discount: u64, + pub builder_fee: Option, } pub fn calculate_fee_for_fulfillment_with_amm( @@ -45,6 +46,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( is_post_only: bool, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let fee_tier = determine_user_fee_tier( user_stats, @@ -92,6 +94,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( filler_reward, referrer_reward: 0, referee_discount: 0, + builder_fee: None, }) } else { let mut fee = calculate_taker_fee(quote_asset_amount, &fee_tier, fee_adjustment)?; @@ -131,6 +134,16 @@ pub fn calculate_fee_for_fulfillment_with_amm( let fee_to_market_for_lp = fee_to_market.safe_sub(quote_asset_amount_surplus)?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + Some( + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(100_000)?, + ) + } else { + None + }; + // must be non-negative Ok(FillFees { user_fee: fee, @@ -140,6 +153,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( filler_reward, referrer_reward, referee_discount, + builder_fee, }) } } @@ -286,6 +300,7 @@ pub fn calculate_fee_for_fulfillment_with_match( market_type: &MarketType, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let taker_fee_tier = determine_user_fee_tier( taker_stats, @@ -337,6 +352,16 @@ pub fn calculate_fee_for_fulfillment_with_match( .safe_sub(maker_rebate)? .cast::()?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + Some( + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(100_000)?, + ) + } else { + None + }; + Ok(FillFees { user_fee: taker_fee, maker_rebate, @@ -345,6 +370,7 @@ pub fn calculate_fee_for_fulfillment_with_match( referrer_reward, fee_to_market_for_lp: 0, referee_discount, + builder_fee, }) } diff --git a/programs/drift/src/math/fees/tests.rs b/programs/drift/src/math/fees/tests.rs index 82188b62b9..296f3bfce5 100644 --- a/programs/drift/src/math/fees/tests.rs +++ b/programs/drift/src/math/fees/tests.rs @@ -31,6 +31,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -75,6 +76,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -118,6 +120,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -161,6 +164,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -202,6 +206,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -240,6 +245,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -271,6 +277,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 50, false, + None, ) .unwrap(); @@ -303,6 +310,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -335,6 +343,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -373,6 +382,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -404,6 +414,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -436,6 +447,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, true, + None, ) .unwrap(); @@ -468,6 +480,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -500,6 +513,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -538,6 +552,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, true, + None, ) .unwrap(); @@ -583,6 +598,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 0, false, + None, ) .unwrap(); @@ -620,6 +636,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -649,6 +666,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 50, false, + None, ) .unwrap(); @@ -679,6 +697,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -709,6 +728,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -746,6 +766,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, true, + None, ) .unwrap(); diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 2d6c20fa7a..9ebe15e0fd 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -256,10 +256,15 @@ pub struct OrderActionRecord { pub maker_existing_base_asset_amount: Option, /// precision: PRICE_PRECISION pub trigger_price: Option, + + /// the idx of the builder in the taker's [`RevenueShareEscrow`] account + pub builder_idx: Option, + /// precision: QUOTE_PRECISION builder fee paid by the taker + pub builder_fee: Option, } impl Size for OrderActionRecord { - const SIZE: usize = 464; + const SIZE: usize = 480; } pub fn get_order_action_record( @@ -288,6 +293,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount: Option, maker_existing_base_asset_amount: Option, trigger_price: Option, + builder_idx: Option, + builder_fee: Option, ) -> DriftResult { Ok(OrderActionRecord { ts, @@ -341,6 +348,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, trigger_price, + builder_idx, + builder_fee, }) } @@ -698,6 +707,23 @@ pub struct FuelSeasonRecord { pub fuel_total: u128, } +#[event] +pub struct RevenueShareSettleRecord { + pub ts: i64, + pub builder: Option, + pub referrer: Option, + pub fee_settled: u64, + pub market_index: u16, + pub market_type: MarketType, + pub builder_sub_account_id: u16, + pub builder_total_referrer_rewards: u64, + pub builder_total_builder_rewards: u64, +} + +impl Size for RevenueShareSettleRecord { + const SIZE: usize = 140; +} + pub fn emit_stack(event: T) -> DriftResult { #[cfg(not(feature = "drift-rs"))] { diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index 69ac0eb312..db5c115036 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -18,6 +18,8 @@ pub mod perp_market; pub mod perp_market_map; pub mod protected_maker_mode_config; pub mod pyth_lazer_oracle; +pub mod revenue_share; +pub mod revenue_share_map; pub mod settle_pnl_mode; pub mod signed_msg_user; pub mod spot_fulfillment_params; diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 3b3431a38c..95ccf8424d 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -873,6 +873,8 @@ pub struct SignedMsgOrderParamsMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -884,6 +886,8 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] diff --git a/programs/drift/src/state/revenue_share.rs b/programs/drift/src/state/revenue_share.rs new file mode 100644 index 0000000000..6d99427054 --- /dev/null +++ b/programs/drift/src/state/revenue_share.rs @@ -0,0 +1,572 @@ +use std::cell::{Ref, RefMut}; + +use anchor_lang::prelude::Pubkey; +use anchor_lang::*; +use anchor_lang::{account, zero_copy}; +use borsh::{BorshDeserialize, BorshSerialize}; +use prelude::AccountInfo; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::user::{MarketType, OrderStatus, User}; +use crate::validate; +use crate::{msg, ID}; + +pub const REVENUE_SHARE_PDA_SEED: &str = "REV_SHARE"; +pub const REVENUE_SHARE_ESCROW_PDA_SEED: &str = "REV_ESCROW"; + +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum RevenueShareOrderBitFlag { + #[default] + Init = 0b00000000, + Open = 0b00000001, + Completed = 0b00000010, + Referral = 0b00000100, +} + +#[account(zero_copy(unsafe))] +#[derive(Eq, PartialEq, Debug, Default)] +pub struct RevenueShare { + /// the owner of this account, a builder or referrer + pub authority: Pubkey, + pub total_referrer_rewards: u64, + pub total_builder_rewards: u64, + pub padding: [u8; 18], +} + +impl RevenueShare { + pub fn space() -> usize { + 8 + 32 + 8 + 8 + 18 + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +pub struct RevenueShareOrder { + /// fees accrued so far for this order slot. This is not exclusively fees from this order_id + /// and may include fees from other orders in the same market. This may be swept to the + /// builder's SpotPosition during settle_pnl. + pub fees_accrued: u64, + /// the order_id of the current active order in this slot. It's only relevant while bit_flag = Open + pub order_id: u32, + /// the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01% + pub fee_tenth_bps: u16, + pub market_index: u16, + /// the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open + pub sub_account_id: u16, + /// the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored + /// if bit_flag = Referral. + pub builder_idx: u8, + /// bitflags that describe the state of the order. + /// [`RevenueShareOrderBitFlag::Init`]: this order slot is available for use. + /// [`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order. + /// [`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into. + /// the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders. + /// [`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market. + /// If it is set, no other bitflag should be set. + pub bit_flags: u8, + /// the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches. + pub user_order_index: u8, + pub market_type: MarketType, + pub padding: [u8; 10], +} + +impl RevenueShareOrder { + pub fn new( + builder_idx: u8, + sub_account_id: u16, + order_id: u32, + fee_tenth_bps: u16, + market_type: MarketType, + market_index: u16, + bit_flags: u8, + user_order_index: u8, + ) -> Self { + Self { + builder_idx, + order_id, + fee_tenth_bps, + market_type, + market_index, + fees_accrued: 0, + bit_flags, + sub_account_id, + user_order_index, + padding: [0; 10], + } + } + + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn add_bit_flag(&mut self, flag: RevenueShareOrderBitFlag) { + self.bit_flags |= flag as u8; + } + + pub fn is_bit_flag_set(&self, flag: RevenueShareOrderBitFlag) -> bool { + (self.bit_flags & flag as u8) != 0 + } + + // An order is Open after it is created, the slot is considered occupied + // and it is waiting to become `Completed` (filled or canceled). + pub fn is_open(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Open) + } + + // An order is Completed after it is filled or canceled. It is waiting to be settled + // into the builder's account + pub fn is_completed(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Completed) + } + + /// An order slot is available (can be written to) if it is neither Completed nor Open. + pub fn is_available(&self) -> bool { + !self.is_completed() && !self.is_open() && !self.is_referral_order() + } + + pub fn is_referral_order(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Referral) + } + + /// Checks if `self` can be merged with `other`. Merged orders track cumulative fees accrued + /// and are settled together, making more efficient use of the orders list. + pub fn is_mergeable(&self, other: &RevenueShareOrder) -> bool { + (self.is_referral_order() == other.is_referral_order()) + && other.is_completed() + && other.market_index == self.market_index + && other.market_type == self.market_type + && other.builder_idx == self.builder_idx + } + + /// Merges `other` into `self`. The orders must be mergeable. + pub fn merge(mut self, other: &RevenueShareOrder) -> DriftResult { + validate!( + self.is_mergeable(other), + ErrorCode::DefaultError, + "Orders are not mergeable" + )?; + self.fees_accrued = self + .fees_accrued + .checked_add(other.fees_accrued) + .ok_or(ErrorCode::MathError)?; + Ok(self) + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct BuilderInfo { + pub authority: Pubkey, // builder authority + pub max_fee_tenth_bps: u16, + pub padding: [u8; 6], +} + +impl BuilderInfo { + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn is_revoked(&self) -> bool { + self.max_fee_tenth_bps == 0 + } +} + +#[account] +#[derive(Eq, PartialEq, Debug)] +#[repr(C)] +pub struct RevenueShareEscrow { + /// the owner of this account, a user + pub authority: Pubkey, + pub referrer: Pubkey, + pub referrer_boost_expire_ts: u32, + pub referrer_reward_offset: i8, + pub referee_fee_numerator_offset: i8, + pub referrer_boost_numerator: i8, + pub reserved_fixed: [u8; 17], + pub padding0: u32, // align with [`RevenueShareEscrow::orders`] 4 bytes len prefix + pub orders: Vec, + pub padding1: u32, // align with [`RevenueShareEscrow::approved_builders`] 4 bytes len prefix + pub approved_builders: Vec, +} + +impl RevenueShareEscrow { + pub fn space(num_orders: usize, num_builders: usize) -> usize { + 8 + // discriminator + std::mem::size_of::() + // fixed header + 4 + // orders Vec length prefix + 4 + // padding0 + num_orders * std::mem::size_of::() + // orders data + 4 + // approved_builders Vec length prefix + 4 + // padding1 + num_builders * std::mem::size_of::() // builders data + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.orders.len() <= 128 && self.approved_builders.len() <= 128, + ErrorCode::DefaultError, + "RevenueShareEscrow orders and approved_builders len must be between 1 and 128" + )?; + Ok(()) + } +} + +#[zero_copy] +#[derive(Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct RevenueShareEscrowFixed { + pub authority: Pubkey, + pub referrer: Pubkey, + pub referrer_boost_expire_ts: u32, + pub referrer_reward_offset: i8, + pub referee_fee_numerator_offset: i8, + pub referrer_boost_numerator: i8, + pub reserved_fixed: [u8; 17], +} + +impl Default for RevenueShareEscrowFixed { + fn default() -> Self { + Self { + authority: Pubkey::default(), + referrer: Pubkey::default(), + referrer_boost_expire_ts: 0, + referrer_reward_offset: 0, + referee_fee_numerator_offset: 0, + referrer_boost_numerator: 0, + reserved_fixed: [0; 17], + } + } +} + +impl Default for RevenueShareEscrow { + fn default() -> Self { + Self { + authority: Pubkey::default(), + referrer: Pubkey::default(), + referrer_boost_expire_ts: 0, + referrer_reward_offset: 0, + referee_fee_numerator_offset: 0, + referrer_boost_numerator: 0, + reserved_fixed: [0; 17], + padding0: 0, + orders: Vec::new(), + padding1: 0, + approved_builders: Vec::new(), + } + } +} + +pub struct RevenueShareEscrowZeroCopy<'a> { + pub fixed: Ref<'a, RevenueShareEscrowFixed>, + pub data: Ref<'a, [u8]>, +} + +impl<'a> RevenueShareEscrowZeroCopy<'a> { + pub fn orders_len(&self) -> u32 { + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + let orders_data_size = + self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + orders_data_size + 4; // RevenueShareEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order(&self, index: u32) -> DriftResult<&RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder(&self, index: u32) -> DriftResult<&BuilderInfo> { + validate!( + index < self.approved_builders_len(), + ErrorCode::DefaultError, + "Builder index out of bounds" + )?; + let size = std::mem::size_of::(); + let offset = 4 + 4 + // Skip orders Vec length prefix + padding0 + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4; // Skip approved_builders Vec length prefix + padding1 + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn iter_orders(&self) -> impl Iterator> + '_ { + (0..self.orders_len()).map(move |i| self.get_order(i)) + } + + pub fn iter_approved_builders(&self) -> impl Iterator> + '_ { + (0..self.approved_builders_len()).map(move |i| self.get_approved_builder(i)) + } +} + +pub struct RevenueShareEscrowZeroCopyMut<'a> { + pub fixed: RefMut<'a, RevenueShareEscrowFixed>, + pub data: RefMut<'a, [u8]>, +} + +impl<'a> RevenueShareEscrowZeroCopyMut<'a> { + pub fn has_referrer(&self) -> bool { + self.fixed.referrer != Pubkey::default() + } + + pub fn get_referrer(&self) -> Option { + if self.has_referrer() { + Some(self.fixed.referrer) + } else { + None + } + } + + pub fn orders_len(&self) -> u32 { + // skip RevenueShareEscrow.padding0 + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + // Calculate offset to the approved_builders Vec length + let orders_data_size = + self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + orders_data_size + + 4; // RevenueShareEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order_mut(&mut self, index: u32) -> DriftResult<&mut RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..(start + size)], + )) + } + + /// Returns the index of an order for a given sub_account_id and order_id, if present. + pub fn find_order_index(&self, sub_account_id: u16, order_id: u32) -> Option { + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.order_id == order_id + && existing_order.sub_account_id == sub_account_id + { + return Some(i); + } + } + } + None + } + + /// Returns the index for the referral order, creating one if necessary. Returns None if a new order + /// cannot be created. + pub fn find_or_create_referral_index(&mut self, market_index: u16) -> Option { + // look for an existing referral order + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.is_referral_order() && existing_order.market_index == market_index + { + return Some(i); + } + } + } + + // try to create a referral order in an available order slot + match self.add_order(RevenueShareOrder::new( + 0, + 0, + 0, + 0, + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Referral as u8, + 0, + )) { + Ok(idx) => Some(idx), + Err(_) => { + msg!("Failed to add referral order, RevenueShareEscrow is full"); + None + } + } + } + + pub fn get_order(&self, index: u32) -> DriftResult<&RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder_mut(&mut self, index: u8) -> DriftResult<&mut BuilderInfo> { + validate!( + index < self.approved_builders_len().cast::()?, + ErrorCode::DefaultError, + "Builder index out of bounds, index: {}, orderslen: {}, builderslen: {}", + index, + self.orders_len(), + self.approved_builders_len() + )?; + let size = std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4 + // RevenueShareEscrow.padding1 + 4; // vec len + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..start + size], + )) + } + + pub fn add_order(&mut self, order: RevenueShareOrder) -> DriftResult { + for i in 0..self.orders_len() { + let existing_order = self.get_order_mut(i)?; + if existing_order.is_mergeable(&order) { + *existing_order = existing_order.merge(&order)?; + return Ok(i); + } else if existing_order.is_available() { + *existing_order = order; + return Ok(i); + } + } + + Err(ErrorCode::RevenueShareEscrowOrdersAccountFull.into()) + } + + /// Marks any [`RevenueShareOrder`]s as Complete if there is no longer a corresponding + /// open order in the user's account. This is used to lazily reconcile state when + /// in place_order and settle_pnl instead of requiring explicit updates on cancels. + pub fn revoke_completed_orders(&mut self, user: &User) -> DriftResult<()> { + for i in 0..self.orders_len() { + if let Ok(rev_share_order) = self.get_order_mut(i) { + if rev_share_order.is_referral_order() { + continue; + } + if user.sub_account_id != rev_share_order.sub_account_id { + continue; + } + if rev_share_order.is_open() && !rev_share_order.is_completed() { + let user_order = user.orders[rev_share_order.user_order_index as usize]; + let still_open = user_order.status == OrderStatus::Open + && user_order.order_id == rev_share_order.order_id; + if !still_open { + if rev_share_order.fees_accrued > 0 { + rev_share_order.add_bit_flag(RevenueShareOrderBitFlag::Completed); + } else { + // order had no fees accrued, we can just clear out the slot + *rev_share_order = RevenueShareOrder::default(); + } + } + } + } + } + + Ok(()) + } +} + +pub trait RevenueShareEscrowLoader<'a> { + fn load_zc(&self) -> DriftResult; + fn load_zc_mut(&self) -> DriftResult; +} + +impl<'a> RevenueShareEscrowLoader<'a> for AccountInfo<'a> { + fn load_zc(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid RevenueShareEscrow owner", + )?; + + let data = self.try_borrow_data().safe_unwrap()?; + + let (discriminator, data) = Ref::map_split(data, |d| d.split_at(8)); + validate!( + *discriminator == RevenueShareEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = Ref::map_split(data, |d| d.split_at(hdr_size)); + Ok(RevenueShareEscrowZeroCopy { + fixed: Ref::map(fixed, |b| bytemuck::from_bytes(b)), + data, + }) + } + + fn load_zc_mut(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid RevenueShareEscrow owner", + )?; + + let data = self.try_borrow_mut_data().safe_unwrap()?; + + let (discriminator, data) = RefMut::map_split(data, |d| d.split_at_mut(8)); + validate!( + *discriminator == RevenueShareEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = RefMut::map_split(data, |d| d.split_at_mut(hdr_size)); + Ok(RevenueShareEscrowZeroCopyMut { + fixed: RefMut::map(fixed, |b| bytemuck::from_bytes_mut(b)), + data, + }) + } +} diff --git a/programs/drift/src/state/revenue_share_map.rs b/programs/drift/src/state/revenue_share_map.rs new file mode 100644 index 0000000000..2e45195040 --- /dev/null +++ b/programs/drift/src/state/revenue_share_map.rs @@ -0,0 +1,209 @@ +use crate::error::{DriftResult, ErrorCode}; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::msg; +use crate::state::revenue_share::RevenueShare; +use crate::state::traits::Size; +use crate::state::user::User; +use crate::validate; +use anchor_lang::prelude::AccountLoader; +use anchor_lang::Discriminator; +use arrayref::array_ref; +use solana_program::account_info::AccountInfo; +use solana_program::pubkey::Pubkey; +use std::cell::RefMut; +use std::collections::BTreeMap; +use std::iter::Peekable; +use std::panic::Location; +use std::slice::Iter; + +pub struct RevenueShareEntry<'a> { + pub user: Option>, + pub revenue_share: Option>, +} + +impl<'a> Default for RevenueShareEntry<'a> { + fn default() -> Self { + Self { + user: None, + revenue_share: None, + } + } +} + +pub struct RevenueShareMap<'a>(pub BTreeMap>); + +impl<'a> RevenueShareMap<'a> { + pub fn empty() -> Self { + RevenueShareMap(BTreeMap::new()) + } + + pub fn insert_user( + &mut self, + authority: Pubkey, + user_loader: AccountLoader<'a, User>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.user.is_none(), + ErrorCode::DefaultError, + "Duplicate User for authority {:?}", + authority + )?; + entry.user = Some(user_loader); + Ok(()) + } + + pub fn insert_revenue_share( + &mut self, + authority: Pubkey, + revenue_share_loader: AccountLoader<'a, RevenueShare>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.revenue_share.is_none(), + ErrorCode::DefaultError, + "Duplicate RevenueShare for authority {:?}", + authority + )?; + entry.revenue_share = Some(revenue_share_loader); + Ok(()) + } + + #[track_caller] + #[inline(always)] + pub fn get_user_ref_mut(&self, authority: &Pubkey) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.user.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UserNotFound); + } + }; + + match loader.load_mut() { + Ok(user) => Ok(user), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadUserAccount) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_revenue_share_account_mut( + &self, + authority: &Pubkey, + ) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.revenue_share.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find revenue share for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UnableToLoadRevenueShareAccount); + } + }; + + match loader.load_mut() { + Ok(revenue_share) => Ok(revenue_share), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load revenue share for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadRevenueShareAccount) + } + } + } +} + +pub fn load_revenue_share_map<'a: 'b, 'b>( + account_info_iter: &mut Peekable>>, +) -> DriftResult> { + let mut revenue_share_map = RevenueShareMap::empty(); + + let user_discriminator: [u8; 8] = User::discriminator(); + let rev_share_discriminator: [u8; 8] = RevenueShare::discriminator(); + + while let Some(account_info) = account_info_iter.peek() { + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::DefaultError))?; + + if data.len() < 8 { + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + + if account_discriminator == &user_discriminator { + let user_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = user_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::UserWrongMutability); + } + + // Extract authority from User account data (after discriminator) + let data = user_account_info + .try_borrow_data() + .or(Err(ErrorCode::CouldNotLoadUserData))?; + let expected_data_len = User::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::CouldNotLoadUserData); + } + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let user_account_loader: AccountLoader = + AccountLoader::try_from(user_account_info) + .or(Err(ErrorCode::InvalidUserAccount))?; + + revenue_share_map.insert_user(authority, user_account_loader)?; + continue; + } + + if account_discriminator == &rev_share_discriminator { + let revenue_share_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = revenue_share_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::DefaultError); + } + + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let revenue_share_account_loader: AccountLoader = + AccountLoader::try_from(revenue_share_account_info) + .or(Err(ErrorCode::InvalidRevenueShareAccount))?; + + revenue_share_map.insert_revenue_share(authority, revenue_share_account_loader)?; + continue; + } + + break; + } + + Ok(revenue_share_map) +} diff --git a/programs/drift/src/state/state.rs b/programs/drift/src/state/state.rs index aeb68953b8..10486403b8 100644 --- a/programs/drift/src/state/state.rs +++ b/programs/drift/src/state/state.rs @@ -122,6 +122,14 @@ impl State { (self.feature_bit_flags & (FeatureBitFlags::MedianTriggerPrice as u8)) > 0 } + pub fn builder_codes_enabled(&self) -> bool { + (self.feature_bit_flags & (FeatureBitFlags::BuilderCodes as u8)) > 0 + } + + pub fn builder_referral_enabled(&self) -> bool { + (self.feature_bit_flags & (FeatureBitFlags::BuilderReferral as u8)) > 0 + } + pub fn allow_settle_lp_pool(&self) -> bool { (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::SettleLpPool as u8)) > 0 } @@ -139,6 +147,8 @@ impl State { pub enum FeatureBitFlags { MmOracleUpdate = 0b00000001, MedianTriggerPrice = 0b00000010, + BuilderCodes = 0b00000100, + BuilderReferral = 0b00001000, } #[derive(Clone, Copy, PartialEq, Debug, Eq)] diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index db6755822a..363efeab3f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -29,6 +29,7 @@ use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; +use bytemuck::{Pod, Zeroable}; use std::cmp::max; use std::fmt; use std::ops::Neg; @@ -1602,12 +1603,16 @@ impl fmt::Display for MarketType { } } +unsafe impl Zeroable for MarketType {} +unsafe impl Pod for MarketType {} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum OrderBitFlag { SignedMessage = 0b00000001, OracleTriggerMarket = 0b00000010, SafeTriggerOrder = 0b00000100, NewTriggerReduceOnly = 0b00001000, + HasBuilder = 0b00010000, } #[account(zero_copy(unsafe))] @@ -1684,6 +1689,7 @@ pub struct UserStats { pub enum ReferrerStatus { IsReferrer = 0b00000001, IsReferred = 0b00000010, + BuilderReferral = 0b00000100, } impl ReferrerStatus { @@ -1694,6 +1700,10 @@ impl ReferrerStatus { pub fn is_referred(status: u8) -> bool { status & ReferrerStatus::IsReferred as u8 != 0 } + + pub fn has_builder_referral(status: u8) -> bool { + status & ReferrerStatus::BuilderReferral as u8 != 0 + } } impl Size for UserStats { @@ -1900,6 +1910,14 @@ impl UserStats { } } + pub fn update_builder_referral_status(&mut self) { + if !self.referrer.eq(&Pubkey::default()) { + self.referrer_status |= ReferrerStatus::BuilderReferral as u8; + } else { + self.referrer_status &= !(ReferrerStatus::BuilderReferral as u8); + } + } + pub fn update_fuel_overflow_status(&mut self, has_overflow: bool) { if has_overflow { self.fuel_overflow_status |= FuelOverflowStatus::Exists as u8; diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 66ba1cab98..a7b1fbcf0a 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -58,6 +58,8 @@ pub struct VerifiedMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, pub signature: [u8; 64], } @@ -96,6 +98,8 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + builder_idx: deserialized.builder_idx, + builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, signature: *signature, }); } else { @@ -123,6 +127,8 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + builder_idx: deserialized.builder_idx, + builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, signature: *signature, }); } diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs index fae2456b43..3c5c2d1c66 100644 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -31,6 +31,9 @@ mod sig_verification { assert!(verified_message.take_profit_order_params.is_none()); assert!(verified_message.stop_loss_order_params.is_none()); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + // Verify order params let order_params = &verified_message.signed_msg_order_params; assert_eq!(order_params.user_order_id, 1); @@ -68,6 +71,8 @@ mod sig_verification { assert_eq!(verified_message.slot, 2345); assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -117,6 +122,8 @@ mod sig_verification { assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_some()); assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -170,6 +177,8 @@ mod sig_verification { assert!(verified_message.take_profit_order_params.is_none()); assert!(verified_message.stop_loss_order_params.is_none()); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); // Verify order params let order_params = &verified_message.signed_msg_order_params; @@ -213,6 +222,8 @@ mod sig_verification { assert_eq!(verified_message.slot, 2345); assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -267,6 +278,11 @@ mod sig_verification { assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_some()); assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -290,4 +306,51 @@ mod sig_verification { assert_eq!(order_params.auction_start_price, Some(240000000i64)); assert_eq!(order_params.auction_end_price, Some(238000000i64)); } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_max_margin_ratio_and_builder_params() { + 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, 0, 1, 255, 255, 1, + 1, 1, 58, 0, + ]; + + // 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_eq!(verified_message.max_margin_ratio.unwrap(), 65535); + assert_eq!(verified_message.builder_idx.unwrap(), 1); + assert_eq!(verified_message.builder_fee_tenth_bps.unwrap(), 58); + + 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_none()); + + // 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)); + } } diff --git a/sdk/VERSION b/sdk/VERSION index 9fb14cec6a..c14ebc75d8 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.140.0-beta.0 \ No newline at end of file +2.141.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index bf99563351..7d0b1f3b3e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.140.0-beta.0", + "version": "2.141.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 2d7926ee4c..7100c16c53 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -399,6 +399,32 @@ export function getIfRebalanceConfigPublicKey( )[0]; } +export function getRevenueShareAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('REV_SHARE')), + authority.toBuffer(), + ], + programId + )[0]; +} + +export function getRevenueShareEscrowAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('REV_ESCROW')), + authority.toBuffer(), + ], + programId + )[0]; +} + export function getLpPoolPublicKey( programId: PublicKey, nameBuffer: number[] diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index f4233bb14a..b9e6f2126c 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4990,6 +4990,189 @@ export class AdminClient extends DriftClient { ); } + public async updateFeatureBitFlagsBuilderCodes( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderCodesIx = + await this.getUpdateFeatureBitFlagsBuilderCodesIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsBuilderCodesIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderCodesIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderCodes(enable, { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + }); + } + + public async updateFeatureBitFlagsBuilderReferral( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderReferralIx = + await this.getUpdateFeatureBitFlagsBuilderReferralIx(enable); + + const tx = await this.buildTransaction( + updateFeatureBitFlagsBuilderReferralIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderReferralIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderReferral( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateFeatureBitFlagsMedianTriggerPrice( + enable: boolean + ): Promise { + const updateFeatureBitFlagsMedianTriggerPriceIx = + await this.getUpdateFeatureBitFlagsMedianTriggerPriceIx(enable); + const tx = await this.buildTransaction( + updateFeatureBitFlagsMedianTriggerPriceIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsMedianTriggerPriceIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsMedianTriggerPrice( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateDelegateUserGovTokenInsuranceStake( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const updateDelegateUserGovTokenInsuranceStakeIx = + await this.getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority, + delegate + ); + + const tx = await this.buildTransaction( + updateDelegateUserGovTokenInsuranceStakeIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const marketIndex = GOV_SPOT_MARKET_INDEX; + const spotMarket = this.getSpotMarketAccount(marketIndex); + const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( + this.program.programId, + delegate, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + this.program.programId, + authority + ); + + const ix = + this.program.instruction.getUpdateDelegateUserGovTokenInsuranceStakeIx({ + accounts: { + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: ifStakeAccountPublicKey, + userStats: userStatsPublicKey, + signer: this.wallet.publicKey, + insuranceFundVault: spotMarket.insuranceFund.vault, + }, + }); + + return ix; + } + + public async depositIntoInsuranceFundStake( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey, + txParams?: TxParams + ): Promise { + const tx = await this.buildTransaction( + await this.getDepositIntoInsuranceFundStakeIx( + marketIndex, + amount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userTokenAccountPublicKey + ), + txParams + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositIntoInsuranceFundStakeIx( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey + ): Promise { + const spotMarket = this.getSpotMarketAccount(marketIndex); + return await this.program.instruction.depositIntoInsuranceFundStake( + marketIndex, + amount, + { + accounts: { + signer: this.wallet.publicKey, + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: insuranceFundStakePublicKey, + userStats: userStatsPublicKey, + spotMarketVault: spotMarket.vault, + insuranceFundVault: spotMarket.insuranceFund.vault, + userTokenAccount: userTokenAccountPublicKey, + tokenProgram: this.getTokenProgramForSpotMarket(spotMarket), + driftSigner: this.getSignerPublicKey(), + }, + } + ); + } + public async updateFeatureBitFlagsSettleLpPool( enable: boolean ): Promise { diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 2e0f8aac86..bbb7a12423 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1325,6 +1325,19 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ '0xa903b5a82cb572397e3d47595d2889cf80513f5b4cf7a36b513ae10cc8b1e338', pythLazerId: 2310, }, + { + fullName: 'PLASMA', + category: ['DEX'], + symbol: 'XPL-PERP', + baseAssetSymbol: 'XPL', + marketIndex: 77, + oracle: new PublicKey('6kgE1KJcxTux4tkPLE8LL8GRyW2cAsvyZsDFWqCrhHVe'), + launchTs: 1758898862000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0x9873512f5cb33c77ad7a5af098d74812c62111166be395fd0941c8cedb9b00d4', + pythLazerId: 2312, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index acbcaab506..d6c84ee73c 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -117,6 +117,8 @@ import { getUserStatsAccountPublicKey, getSignedMsgWsDelegatesAccountPublicKey, getIfRebalanceConfigPublicKey, + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, getConstituentTargetBasePublicKey, getAmmConstituentMappingPublicKey, getLpPoolPublicKey, @@ -137,6 +139,7 @@ import { TxSender, TxSigAndSlot } from './tx/types'; import { BASE_PRECISION, GOV_SPOT_MARKET_INDEX, + MARGIN_PRECISION, ONE, PERCENTAGE_PRECISION, PRICE_PRECISION, @@ -208,6 +211,12 @@ import { SignedMsgOrderParams } from './types'; import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; import { ConstituentMap } from './constituentMap/constituentMap'; +import { hasBuilder } from './math/orders'; +import { RevenueShareEscrowMap } from './userMap/revenueShareEscrowMap'; +import { + isBuilderOrderReferral, + isBuilderOrderCompleted, +} from './math/builder'; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -1240,6 +1249,176 @@ export class DriftClient { return ix; } + public async initializeRevenueShare( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeRevenueShareIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeRevenueShareIx( + authority: PublicKey + ): Promise { + const revenueShare = getRevenueShareAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeRevenueShare({ + accounts: { + revenueShare, + authority, + payer: this.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async initializeRevenueShareEscrow( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeRevenueShareEscrowIx( + authority, + numOrders + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeRevenueShareEscrowIx( + authority: PublicKey, + numOrders: number + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeRevenueShareEscrow(numOrders, { + accounts: { + escrow, + authority, + payer: this.wallet.publicKey, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async migrateReferrer( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getMigrateReferrerIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getMigrateReferrerIx( + authority: PublicKey + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.migrateReferrer({ + accounts: { + escrow, + authority, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + payer: this.wallet.publicKey, + }, + }); + } + + public async resizeRevenueShareEscrowOrders( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getResizeRevenueShareEscrowOrdersIx( + authority, + numOrders + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getResizeRevenueShareEscrowOrdersIx( + authority: PublicKey, + numOrders: number + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.resizeRevenueShareEscrowOrders(numOrders, { + accounts: { + escrow, + authority, + payer: this.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async changeApprovedBuilder( + builder: PublicKey, + maxFeeTenthBps: number, + add: boolean, + txParams?: TxParams + ): Promise { + const ix = await this.getChangeApprovedBuilderIx( + builder, + maxFeeTenthBps, + add + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getChangeApprovedBuilderIx( + builder: PublicKey, + maxFeeTenthBps: number, + add: boolean + ): Promise { + const authority = this.wallet.publicKey; + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.changeApprovedBuilder( + builder, + maxFeeTenthBps, + add, + { + accounts: { + escrow, + authority, + payer: this.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + } + ); + } + public async addSignedMsgWsDelegate( authority: PublicKey, delegate: PublicKey, @@ -1553,11 +1732,11 @@ export class DriftClient { ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, - this.wallet.publicKey, + this.authority, subAccountId ); - await this.addUser(subAccountId, this.wallet.publicKey); + await this.addUser(subAccountId, this.authority); const ix = this.program.instruction.updateUserPerpPositionCustomMarginRatio( subAccountId, @@ -1578,14 +1757,21 @@ export class DriftClient { perpMarketIndex: number, marginRatio: number, subAccountId = 0, - txParams?: TxParams + txParams?: TxParams, + enterHighLeverageMode?: boolean ): Promise { - const ix = await this.getUpdateUserPerpPositionCustomMarginRatioIx( + const ixs = []; + if (enterHighLeverageMode) { + const enableIx = await this.getEnableHighLeverageModeIx(subAccountId); + ixs.push(enableIx); + } + const updateIx = await this.getUpdateUserPerpPositionCustomMarginRatioIx( perpMarketIndex, marginRatio, subAccountId ); - const tx = await this.buildTransaction(ix, txParams ?? this.txParams); + ixs.push(updateIx); + const tx = await this.buildTransaction(ixs, txParams ?? this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } @@ -1967,6 +2153,20 @@ export class DriftClient { writableSpotMarketIndexes, }); + for (const order of userAccount.orders) { + if (hasBuilder(order)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + break; + } + } + const tokenPrograms = new Set(); for (const spotPosition of userAccount.spotPositions) { if (isSpotPositionAvailable(spotPosition)) { @@ -2468,6 +2668,35 @@ export class DriftClient { } } + addBuilderToRemainingAccounts( + builders: PublicKey[], + remainingAccounts: AccountMeta[] + ): void { + for (const builder of builders) { + // Add User account for the builder + const builderUserAccount = getUserAccountPublicKeySync( + this.program.programId, + builder, + 0 // subAccountId 0 for builder user account + ); + remainingAccounts.push({ + pubkey: builderUserAccount, + isSigner: false, + isWritable: true, + }); + + const builderAccount = getRevenueShareAccountPublicKey( + this.program.programId, + builder + ); + remainingAccounts.push({ + pubkey: builderAccount, + isSigner: false, + isWritable: true, + }); + } + } + getRemainingAccountMapsForUsers(userAccounts: UserAccount[]): { oracleAccountMap: Map; spotMarketAccountMap: Map; @@ -4081,7 +4310,8 @@ export class DriftClient { bracketOrdersParams = new Array(), referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, - settlePnl?: boolean + settlePnl?: boolean, + positionMaxLev?: number ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; settlePnlTx?: Transaction | VersionedTransaction; @@ -4097,7 +4327,10 @@ export class DriftClient { const marketIndex = orderParams.marketIndex; const orderId = userAccount.nextOrderId; - const ixPromisesForTxs: Record> = { + const ixPromisesForTxs: Record< + TxKeys, + Promise + > = { cancelExistingOrdersTx: undefined, settlePnlTx: undefined, fillTx: undefined, @@ -4106,10 +4339,18 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - ixPromisesForTxs.marketOrderTx = this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); + const marketOrderTxIxs = positionMaxLev + ? this.getPlaceOrdersAndSetPositionMaxLevIx( + [orderParams, ...bracketOrdersParams], + positionMaxLev, + userAccount.subAccountId + ) + : this.getPlaceOrdersIx( + [orderParams, ...bracketOrdersParams], + userAccount.subAccountId + ); + + ixPromisesForTxs.marketOrderTx = marketOrderTxIxs; /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { @@ -4150,7 +4391,10 @@ export class DriftClient { const ixsMap = ixs.reduce((acc, ix, i) => { acc[txKeys[i]] = ix; return acc; - }, {}) as MappedRecord; + }, {}) as MappedRecord< + typeof ixPromisesForTxs, + TransactionInstruction | TransactionInstruction[] + >; const txsMap = (await this.buildTransactionsMap( ixsMap, @@ -4734,6 +4978,73 @@ export class DriftClient { }); } + public async getPlaceOrdersAndSetPositionMaxLevIx( + params: OptionalOrderParams[], + positionMaxLev: number, + subAccountId?: number + ): Promise { + const user = await this.getUserAccountPublicKey(subAccountId); + + const readablePerpMarketIndex: number[] = []; + const readableSpotMarketIndexes: number[] = []; + for (const param of params) { + if (!param.marketType) { + throw new Error('must set param.marketType'); + } + if (isVariant(param.marketType, 'perp')) { + readablePerpMarketIndex.push(param.marketIndex); + } else { + readableSpotMarketIndexes.push(param.marketIndex); + } + } + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + readablePerpMarketIndex, + readableSpotMarketIndexes, + useMarketLastSlotCache: true, + }); + + for (const param of params) { + if (isUpdateHighLeverageMode(param.bitFlags)) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + } + + const formattedParams = params.map((item) => getOrderParams(item)); + + const placeOrdersIxs = await this.program.instruction.placeOrders( + formattedParams, + { + accounts: { + state: await this.getStatePublicKey(), + user, + userStats: this.getUserStatsAccountPublicKey(), + authority: this.wallet.publicKey, + }, + remainingAccounts, + } + ); + + const marginRatio = Math.floor( + (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() + ); + + // TODO: Handle multiple markets? + const setPositionMaxLevIxs = + await this.getUpdateUserPerpPositionCustomMarginRatioIx( + readablePerpMarketIndex[0], + marginRatio, + subAccountId + ); + + return [placeOrdersIxs, setPositionMaxLevIxs]; + } + public async fillPerpOrder( userAccountPublicKey: PublicKey, user: UserAccount, @@ -4742,7 +5053,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, txParams?: TxParams, fillerSubAccountId?: number, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( @@ -4754,7 +5066,8 @@ export class DriftClient { referrerInfo, fillerSubAccountId, undefined, - fillerAuthority + fillerAuthority, + hasBuilderFee ), txParams ), @@ -4772,7 +5085,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, fillerSubAccountId?: number, isSignedMsg?: boolean, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, @@ -4854,6 +5168,36 @@ export class DriftClient { } } + let withBuilder = false; + if (hasBuilderFee) { + withBuilder = true; + } else { + // figure out if we need builder account or not + if (order && !isSignedMsg) { + const userOrder = userAccount.orders.find( + (o) => o.orderId === order.orderId + ); + if (userOrder) { + withBuilder = hasBuilder(userOrder); + } + } else if (isSignedMsg) { + // Order hasn't been placed yet, we cant tell if it has a builder or not. + // Include it optimistically + withBuilder = true; + } + } + + if (withBuilder) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + const orderId = isSignedMsg ? null : order.orderId; return await this.program.instruction.fillPerpOrder(orderId, null, { accounts: { @@ -6473,7 +6817,26 @@ export class DriftClient { }); } + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + const takerOrderId = takerInfo.order.orderId; + if (hasBuilder(takerInfo.order)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } return await this.program.instruction.placeAndMakePerpOrder( orderParams, takerOrderId, @@ -6559,16 +6922,29 @@ export class DriftClient { ? 'global' + ':' + 'SignedMsgOrderParamsDelegateMessage' : 'global' + ':' + 'SignedMsgOrderParamsMessage'; const prefix = Buffer.from(sha256(anchorIxName).slice(0, 8)); + + // Backwards-compat: normalize optional builder fields to null for encoding + const withBuilderDefaults = { + ...orderParamsMessage, + builderIdx: + orderParamsMessage.builderIdx !== undefined + ? orderParamsMessage.builderIdx + : null, + builderFeeTenthBps: + orderParamsMessage.builderFeeTenthBps !== undefined + ? orderParamsMessage.builderFeeTenthBps + : null, + }; const buf = Buffer.concat([ prefix, delegateSigner ? this.program.coder.types.encode( 'SignedMsgOrderParamsDelegateMessage', - orderParamsMessage as SignedMsgOrderParamsDelegateMessage + withBuilderDefaults as SignedMsgOrderParamsDelegateMessage ) : this.program.coder.types.encode( 'SignedMsgOrderParamsMessage', - orderParamsMessage as SignedMsgOrderParamsMessage + withBuilderDefaults as SignedMsgOrderParamsMessage ), ]); return buf; @@ -6657,20 +7033,30 @@ export class DriftClient { signedSignedMsgOrderParams.orderParams.toString(), 'hex' ); - try { - const { signedMsgOrderParams } = this.decodeSignedMsgOrderParamsMessage( - borshBuf, - isDelegateSigner - ); - if (isUpdateHighLeverageMode(signedMsgOrderParams.bitFlags)) { - remainingAccounts.push({ - pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), - isWritable: true, - isSigner: false, - }); - } - } catch (err) { - console.error('invalid signed order encoding'); + + const signedMessage = this.decodeSignedMsgOrderParamsMessage( + borshBuf, + isDelegateSigner + ); + if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + if ( + signedMessage.builderFeeTenthBps !== null && + signedMessage.builderIdx !== null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); } const messageLengthBuffer = Buffer.alloc(2); @@ -6801,6 +7187,32 @@ export class DriftClient { }); } + const isDelegateSigner = takerInfo.signingAuthority.equals( + takerInfo.takerUserAccount.delegate + ); + const borshBuf = Buffer.from( + signedSignedMsgOrderParams.orderParams.toString(), + 'hex' + ); + + const signedMessage = this.decodeSignedMsgOrderParamsMessage( + borshBuf, + isDelegateSigner + ); + if ( + signedMessage.builderFeeTenthBps !== null && + signedMessage.builderIdx !== null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + const placeAndMakeIx = await this.program.instruction.placeAndMakeSignedMsgPerpOrder( orderParams, @@ -7457,7 +7869,8 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndex: number, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + escrowMap?: RevenueShareEscrowMap ): Promise { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); @@ -7466,7 +7879,8 @@ export class DriftClient { await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, - marketIndex + marketIndex, + escrowMap ), txParams, undefined, @@ -7484,7 +7898,8 @@ export class DriftClient { public async settlePNLIx( settleeUserAccountPublicKey: PublicKey, settleeUserAccount: UserAccount, - marketIndex: number + marketIndex: number, + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7492,6 +7907,89 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (revenueShareEscrowMap) { + const escrow = revenueShareEscrowMap.get( + settleeUserAccount.authority.toBase58() + ); + if (escrow) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + + const builders = new Map(); + for (const order of escrow.orders) { + const eligibleBuilder = + isBuilderOrderCompleted(order) && + !isBuilderOrderReferral(order) && + order.feesAccrued.gt(ZERO) && + order.marketIndex === marketIndex; + if (eligibleBuilder && !builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + escrow.approvedBuilders[order.builderIdx].authority + ); + } + } + if (builders.size > 0) { + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + + // Include escrow and referrer accounts if referral rewards exist for this market + const hasReferralForMarket = escrow.orders.some( + (o) => + isBuilderOrderReferral(o) && + o.feesAccrued.gt(ZERO) && + o.marketIndex === marketIndex + ); + + if (hasReferralForMarket) { + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + if (!escrow.referrer.equals(PublicKey.default)) { + this.addBuilderToRemainingAccounts( + [escrow.referrer], + remainingAccounts + ); + } + } + } else { + // Stale-cache fallback: if the user has any builder orders, include escrow PDA. This allows + // the program to lazily clean up any completed builder orders. + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + break; + } + } + } + } + return await this.program.instruction.settlePnl(marketIndex, { accounts: { state: await this.getStatePublicKey(), @@ -7508,6 +8006,7 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndexes: number[], mode: SettlePnlMode, + revenueShareEscrowMap?: RevenueShareEscrowMap, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( @@ -7516,7 +8015,9 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + undefined, + revenueShareEscrowMap ), txParams ), @@ -7532,7 +8033,8 @@ export class DriftClient { marketIndexes: number[], mode: SettlePnlMode, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { // need multiple TXs because settling more than 4 markets won't fit in a single TX const txsToSign: (Transaction | VersionedTransaction)[] = []; @@ -7546,7 +8048,9 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + undefined, + revenueShareEscrowMap ); const computeUnits = Math.min(300_000 * marketIndexes.length, 1_400_000); const tx = await this.buildTransaction( @@ -7591,7 +8095,8 @@ export class DriftClient { mode: SettlePnlMode, overrides?: { authority?: PublicKey; - } + }, + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7599,6 +8104,95 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (revenueShareEscrowMap) { + const escrow = revenueShareEscrowMap.get( + settleeUserAccount.authority.toBase58() + ); + const builders = new Map(); + if (escrow) { + for (const order of escrow.orders) { + const eligibleBuilder = + isBuilderOrderCompleted(order) && + !isBuilderOrderReferral(order) && + order.feesAccrued.gt(ZERO) && + marketIndexes.includes(order.marketIndex); + if (eligibleBuilder && !builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + escrow.approvedBuilders[order.builderIdx].authority + ); + } + } + if (builders.size > 0) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + + // Include escrow and referrer accounts when there are referral rewards + // for any of the markets we are settling, so on-chain sweep can find them. + const hasReferralForRequestedMarkets = escrow.orders.some( + (o) => + isBuilderOrderReferral(o) && + o.feesAccrued.gt(ZERO) && + marketIndexes.includes(o.marketIndex) + ); + + if (hasReferralForRequestedMarkets) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + + // Add referrer's User and RevenueShare accounts + if (!escrow.referrer.equals(PublicKey.default)) { + this.addBuilderToRemainingAccounts( + [escrow.referrer], + remainingAccounts + ); + } + } + } else { + // Stale-cache fallback: if the user has any builder orders, include escrow PDA. This allows + // the program to lazily clean up any completed builder orders. + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + break; + } + } + } + } + return await this.program.instruction.settleMultiplePnls( marketIndexes, mode, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 79a3484def..8914098c0a 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.139.0", + "version": "2.140.0", "name": "drift", "instructions": [ { @@ -7654,6 +7654,174 @@ } ] }, + { + "name": "updateFeatureBitFlagsBuilderCodes", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "initializeRevenueShare", + "accounts": [ + { + "name": "revenueShare", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeRevenueShareEscrow", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "resizeRevenueShareEscrowOrders", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "changeApprovedBuilder", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "builder", + "type": "publicKey" + }, + { + "name": "maxFeeBps", + "type": "u16" + }, + { + "name": "add", + "type": "bool" + } + ] + }, { "name": "updateFeatureBitFlagsSettleLpPool", "accounts": [ @@ -10393,6 +10561,106 @@ ] } }, + { + "name": "RevenueShare", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "the owner of this account, a builder or referrer" + ], + "type": "publicKey" + }, + { + "name": "totalReferrerRewards", + "type": "u64" + }, + { + "name": "totalBuilderRewards", + "type": "u64" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 18 + ] + } + } + ] + } + }, + { + "name": "RevenueShareEscrow", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "the owner of this account, a user" + ], + "type": "publicKey" + }, + { + "name": "referrer", + "type": "publicKey" + }, + { + "name": "referrerBoostExpireTs", + "type": "u32" + }, + { + "name": "referrerRewardOffset", + "type": "i8" + }, + { + "name": "refereeFeeNumeratorOffset", + "type": "i8" + }, + { + "name": "referrerBoostNumerator", + "type": "i8" + }, + { + "name": "reservedFixed", + "type": { + "array": [ + "u8", + 17 + ] + } + }, + { + "name": "padding0", + "type": "u32" + }, + { + "name": "orders", + "type": { + "vec": { + "defined": "RevenueShareOrder" + } + } + }, + { + "name": "padding1", + "type": "u32" + }, + { + "name": "approvedBuilders", + "type": { + "vec": { + "defined": "BuilderInfo" + } + } + } + ] + } + }, { "name": "SignedMsgUserOrders", "docs": [ @@ -12692,6 +12960,18 @@ "type": { "option": "u16" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } } ] } @@ -12745,6 +13025,18 @@ "type": { "option": "u16" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } } ] } @@ -13597,6 +13889,157 @@ ] } }, + { + "name": "RevenueShareOrder", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feesAccrued", + "docs": [ + "fees accrued so far for this order slot. This is not exclusively fees from this order_id", + "and may include fees from other orders in the same market. This may be swept to the", + "builder's SpotPosition during settle_pnl." + ], + "type": "u64" + }, + { + "name": "orderId", + "docs": [ + "the order_id of the current active order in this slot. It's only relevant while bit_flag = Open" + ], + "type": "u32" + }, + { + "name": "feeTenthBps", + "docs": [ + "the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01%" + ], + "type": "u16" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "subAccountId", + "docs": [ + "the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open" + ], + "type": "u16" + }, + { + "name": "builderIdx", + "docs": [ + "the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored", + "if bit_flag = Referral." + ], + "type": "u8" + }, + { + "name": "bitFlags", + "docs": [ + "bitflags that describe the state of the order.", + "[`RevenueShareOrderBitFlag::Init`]: this order slot is available for use.", + "[`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order.", + "[`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into.", + "the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders.", + "[`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market.", + "If it is set, no other bitflag should be set." + ], + "type": "u8" + }, + { + "name": "userOrderIndex", + "docs": [ + "the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches." + ], + "type": "u8" + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 10 + ] + } + } + ] + } + }, + { + "name": "BuilderInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "maxFeeTenthBps", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 6 + ] + } + } + ] + } + }, + { + "name": "RevenueShareEscrowFixed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "referrer", + "type": "publicKey" + }, + { + "name": "referrerBoostExpireTs", + "type": "u32" + }, + { + "name": "referrerRewardOffset", + "type": "i8" + }, + { + "name": "refereeFeeNumeratorOffset", + "type": "i8" + }, + { + "name": "referrerBoostNumerator", + "type": "i8" + }, + { + "name": "reservedFixed", + "type": { + "array": [ + "u8", + 17 + ] + } + } + ] + } + }, { "name": "SignedMsgOrderId", "type": { @@ -15270,6 +15713,26 @@ ] } }, + { + "name": "RevenueShareOrderBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Init" + }, + { + "name": "Open" + }, + { + "name": "Completed" + }, + { + "name": "Referral" + } + ] + } + }, { "name": "SettlePnlMode", "type": { @@ -15391,6 +15854,12 @@ }, { "name": "MedianTriggerPrice" + }, + { + "name": "BuilderCodes" + }, + { + "name": "BuilderReferral" } ] } @@ -15542,6 +16011,9 @@ }, { "name": "NewTriggerReduceOnly" + }, + { + "name": "HasBuilder" } ] } @@ -15556,6 +16028,9 @@ }, { "name": "IsReferred" + }, + { + "name": "BuilderReferral" } ] } @@ -17062,6 +17537,62 @@ } ] }, + { + "name": "RevenueShareSettleRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "builder", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "referrer", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "feeSettled", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + }, + "index": false + }, + { + "name": "builderSubAccountId", + "type": "u16", + "index": false + }, + { + "name": "builderTotalReferrerRewards", + "type": "u64", + "index": false + }, + { + "name": "builderTotalBuilderRewards", + "type": "u64", + "index": false + } + ] + }, { "name": "LPSettleRecord", "fields": [ @@ -18916,106 +19447,146 @@ }, { "code": 6317, + "name": "InvalidRevenueShareResize", + "msg": "Invalid RevenueShare resize" + }, + { + "code": 6318, + "name": "BuilderRevoked", + "msg": "Builder has been revoked" + }, + { + "code": 6319, + "name": "InvalidBuilderFee", + "msg": "Builder fee is greater than max fee bps" + }, + { + "code": 6320, + "name": "RevenueShareEscrowAuthorityMismatch", + "msg": "RevenueShareEscrow authority mismatch" + }, + { + "code": 6321, + "name": "RevenueShareEscrowOrdersAccountFull", + "msg": "RevenueShareEscrow has too many active orders" + }, + { + "code": 6322, + "name": "InvalidRevenueShareAccount", + "msg": "Invalid RevenueShareAccount" + }, + { + "code": 6323, + "name": "CannotRevokeBuilderWithOpenOrders", + "msg": "Cannot revoke builder with open orders" + }, + { + "code": 6324, + "name": "UnableToLoadRevenueShareAccount", + "msg": "Unable to load builder account" + }, + { + "code": 6325, "name": "InvalidConstituent", "msg": "Invalid Constituent" }, { - "code": 6318, + "code": 6326, "name": "InvalidAmmConstituentMappingArgument", "msg": "Invalid Amm Constituent Mapping argument" }, { - "code": 6319, + "code": 6327, "name": "InvalidUpdateConstituentTargetBaseArgument", "msg": "Invalid update constituent update target weights argument" }, { - "code": 6320, + "code": 6328, "name": "ConstituentNotFound", "msg": "Constituent not found" }, { - "code": 6321, + "code": 6329, "name": "ConstituentCouldNotLoad", "msg": "Constituent could not load" }, { - "code": 6322, + "code": 6330, "name": "ConstituentWrongMutability", "msg": "Constituent wrong mutability" }, { - "code": 6323, + "code": 6331, "name": "WrongNumberOfConstituents", "msg": "Wrong number of constituents passed to instruction" }, { - "code": 6324, + "code": 6332, "name": "OracleTooStaleForLPAUMUpdate", "msg": "Oracle too stale for LP AUM update" }, { - "code": 6325, + "code": 6333, "name": "InsufficientConstituentTokenBalance", "msg": "Insufficient constituent token balance" }, { - "code": 6326, + "code": 6334, "name": "AMMCacheStale", "msg": "Amm Cache data too stale" }, { - "code": 6327, + "code": 6335, "name": "LpPoolAumDelayed", "msg": "LP Pool AUM not updated recently" }, { - "code": 6328, + "code": 6336, "name": "ConstituentOracleStale", "msg": "Constituent oracle is stale" }, { - "code": 6329, + "code": 6337, "name": "LpInvariantFailed", "msg": "LP Invariant failed" }, { - "code": 6330, + "code": 6338, "name": "InvalidConstituentDerivativeWeights", "msg": "Invalid constituent derivative weights" }, { - "code": 6331, + "code": 6339, "name": "UnauthorizedDlpAuthority", "msg": "Unauthorized dlp authority" }, { - "code": 6332, + "code": 6340, "name": "MaxDlpAumBreached", "msg": "Max DLP AUM Breached" }, { - "code": 6333, + "code": 6341, "name": "SettleLpPoolDisabled", "msg": "Settle Lp Pool Disabled" }, { - "code": 6334, + "code": 6342, "name": "MintRedeemLpPoolDisabled", "msg": "Mint/Redeem Lp Pool Disabled" }, { - "code": 6335, + "code": 6343, "name": "LpPoolSettleInvariantBreached", "msg": "Settlement amount exceeded" }, { - "code": 6336, + "code": 6344, "name": "InvalidConstituentOperation", "msg": "Invalid constituent operation" }, { - "code": 6337, + "code": 6345, "name": "Unauthorized", "msg": "Unauthorized for operation" } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 61d803a92c..1119237a06 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -117,6 +117,7 @@ export * from './dlob/orderBookLevels'; export * from './userMap/userMap'; export * from './userMap/referrerMap'; export * from './userMap/userStatsMap'; +export * from './userMap/revenueShareEscrowMap'; export * from './userMap/userMapConfig'; export * from './math/bankruptcy'; export * from './orderSubscriber'; diff --git a/sdk/src/math/builder.ts b/sdk/src/math/builder.ts new file mode 100644 index 0000000000..75681a1823 --- /dev/null +++ b/sdk/src/math/builder.ts @@ -0,0 +1,20 @@ +import { RevenueShareOrder } from '../types'; + +const FLAG_IS_OPEN = 0x01; +export function isBuilderOrderOpen(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_OPEN) !== 0; +} + +const FLAG_IS_COMPLETED = 0x02; +export function isBuilderOrderCompleted(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_COMPLETED) !== 0; +} + +const FLAG_IS_REFERRAL = 0x04; +export function isBuilderOrderReferral(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_REFERRAL) !== 0; +} + +export function isBuilderOrderAvailable(order: RevenueShareOrder): boolean { + return !isBuilderOrderOpen(order) && !isBuilderOrderCompleted(order); +} diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 5bfa6370bd..de5486694b 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -389,6 +389,11 @@ export function isSignedMsgOrder(order: Order): boolean { return (order.bitFlags & FLAG_IS_SIGNED_MSG) !== 0; } +const FLAG_HAS_BUILDER = 0x10; +export function hasBuilder(order: Order): boolean { + return (order.bitFlags & FLAG_HAS_BUILDER) !== 0; +} + export function calculateOrderBaseAssetAmount( order: Order, existingBaseAssetAmount: BN diff --git a/sdk/src/math/state.ts b/sdk/src/math/state.ts index f4414214a9..0565f959ba 100644 --- a/sdk/src/math/state.ts +++ b/sdk/src/math/state.ts @@ -38,3 +38,11 @@ export function useMedianTriggerPrice(stateAccount: StateAccount): boolean { (stateAccount.featureBitFlags & FeatureBitFlags.MEDIAN_TRIGGER_PRICE) > 0 ); } + +export function builderCodesEnabled(stateAccount: StateAccount): boolean { + return (stateAccount.featureBitFlags & FeatureBitFlags.BUILDER_CODES) > 0; +} + +export function builderReferralEnabled(stateAccount: StateAccount): boolean { + return (stateAccount.featureBitFlags & FeatureBitFlags.BUILDER_REFERRAL) > 0; +} diff --git a/sdk/src/memcmp.ts b/sdk/src/memcmp.ts index 895c0a839b..c73d21efdb 100644 --- a/sdk/src/memcmp.ts +++ b/sdk/src/memcmp.ts @@ -130,6 +130,17 @@ export function getSpotMarketAccountsFilter(): MemcmpFilter { }; } +export function getRevenueShareEscrowFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('RevenueShareEscrow') + ), + }, + }; +} + export function getConstituentFilter(): MemcmpFilter { return { memcmp: { diff --git a/sdk/src/swift/swiftOrderSubscriber.ts b/sdk/src/swift/swiftOrderSubscriber.ts index 540691521d..eac7e3893c 100644 --- a/sdk/src/swift/swiftOrderSubscriber.ts +++ b/sdk/src/swift/swiftOrderSubscriber.ts @@ -201,9 +201,7 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = + const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage( signedMsgOrderParamsBuf, isDelegateSigner @@ -281,13 +279,10 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = - this.driftClient.decodeSignedMsgOrderParamsMessage( - signedMsgOrderParamsBuf, - isDelegateSigner - ); + const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage( + signedMsgOrderParamsBuf, + isDelegateSigner + ); const takerAuthority = new PublicKey(orderMessageRaw.taker_authority); const signingAuthority = new PublicKey(orderMessageRaw.signing_authority); diff --git a/sdk/src/types.ts b/sdk/src/types.ts index f4f41dcc8f..8ddd31a750 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -31,6 +31,8 @@ export enum ExchangeStatus { export enum FeatureBitFlags { MM_ORACLE_UPDATE = 1, MEDIAN_TRIGGER_PRICE = 2, + BUILDER_CODES = 4, + BUILDER_REFERRAL = 8, } export class MarketStatus { @@ -1314,6 +1316,8 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + builderIdx?: number | null; + builderFeeTenthBps?: number | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1324,6 +1328,8 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + builderIdx?: number | null; + builderFeeTenthBps?: number | null; }; export type SignedMsgTriggerOrderParams = { @@ -1643,6 +1649,54 @@ export type SignedMsgUserOrdersAccount = { signedMsgOrderData: SignedMsgOrderId[]; }; +export type RevenueShareAccount = { + authority: PublicKey; + totalReferrerRewards: BN; + totalBuilderRewards: BN; + padding: number[]; +}; + +export type RevenueShareEscrowAccount = { + authority: PublicKey; + referrer: PublicKey; + referrerBoostExpireTs: number; + referrerRewardOffset: number; + refereeFeeNumeratorOffset: number; + referrerBoostNumerator: number; + reservedFixed: number[]; + orders: RevenueShareOrder[]; + approvedBuilders: BuilderInfo[]; +}; + +export type RevenueShareOrder = { + builderIdx: number; + feesAccrued: BN; + orderId: number; + feeTenthBps: number; + marketIndex: number; + bitFlags: number; + marketType: MarketType; // 0: spot, 1: perp + padding: number[]; +}; + +export type BuilderInfo = { + authority: PublicKey; + maxFeeTenthBps: number; + padding: number[]; +}; + +export type RevenueShareSettleRecord = { + ts: number; + builder: PublicKey | null; + referrer: PublicKey | null; + feeSettled: BN; + marketIndex: number; + marketType: MarketType; + builderTotalReferrerRewards: BN; + builderTotalBuilderRewards: BN; + builderSubAccountId: number; +}; + export type AddAmmConstituentMappingDatum = { constituentIndex: number; perpMarketIndex: number; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 08494f71f8..a36820306d 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -449,7 +449,8 @@ export class User { public getPerpBuyingPower( marketIndex: number, collateralBuffer = ZERO, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): BN { const perpPosition = this.getPerpPositionOrEmpty(marketIndex); @@ -473,7 +474,7 @@ export class User { freeCollateral, worstCaseBaseAssetAmount, enterHighLeverageMode, - perpPosition + maxMarginRatio || perpPosition.maxMarginRatio ); } @@ -482,17 +483,17 @@ export class User { freeCollateral: BN, baseAssetAmount: BN, enterHighLeverageMode = undefined, - perpPosition?: PerpPosition + perpMarketMaxMarginRatio = undefined ): BN { - const userCustomMargin = Math.max( - perpPosition?.maxMarginRatio ?? 0, + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, this.getUserAccount().maxMarginRatio ); const marginRatio = calculateMarketMarginRatio( this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', - userCustomMargin, + maxMarginRatio, enterHighLeverageMode || this.isHighLeverageMode('Initial') ); @@ -1247,7 +1248,10 @@ export class User { } if (marginCategory) { - const userCustomMargin = this.getUserAccount().maxMarginRatio; + const userCustomMargin = Math.max( + perpPosition.maxMarginRatio, + this.getUserAccount().maxMarginRatio + ); let marginRatio = new BN( calculateMarketMarginRatio( market, @@ -2345,13 +2349,18 @@ export class User { public getMarginUSDCRequiredForTrade( targetMarketIndex: number, baseSize: BN, - estEntryPrice?: BN + estEntryPrice?: BN, + perpMarketMaxMarginRatio?: number ): BN { + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, + this.getUserAccount().maxMarginRatio + ); return calculateMarginUSDCRequiredForTrade( this.driftClient, targetMarketIndex, baseSize, - this.getUserAccount().maxMarginRatio, + maxMarginRatio, undefined, estEntryPrice ); @@ -2360,14 +2369,19 @@ export class User { public getCollateralDepositRequiredForTrade( targetMarketIndex: number, baseSize: BN, - collateralIndex: number + collateralIndex: number, + perpMarketMaxMarginRatio?: number ): BN { + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, + this.getUserAccount().maxMarginRatio + ); return calculateCollateralDepositRequiredForTrade( this.driftClient, targetMarketIndex, baseSize, collateralIndex, - this.getUserAccount().maxMarginRatio, + maxMarginRatio, false // assume user cant be high leverage if they havent created user account ? ); } @@ -2385,7 +2399,8 @@ export class User { targetMarketIndex: number, tradeSide: PositionDirection, isLp = false, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): { tradeSize: BN; oppositeSideTradeSize: BN } { let tradeSize = ZERO; let oppositeSideTradeSize = ZERO; @@ -2424,7 +2439,8 @@ export class User { const maxPositionSize = this.getPerpBuyingPower( targetMarketIndex, lpBuffer, - enterHighLeverageMode + enterHighLeverageMode, + maxMarginRatio ); if (maxPositionSize.gte(ZERO)) { @@ -2451,8 +2467,12 @@ export class User { const marginRequirement = this.getInitialMarginRequirement( enterHighLeverageMode ); + const marginRatio = Math.max( + currentPosition.maxMarginRatio, + this.getUserAccount().maxMarginRatio + ); const marginFreedByClosing = perpLiabilityValue - .mul(new BN(market.marginRatioInitial)) + .mul(new BN(marginRatio)) .div(MARGIN_PRECISION); const marginRequirementAfterClosing = marginRequirement.sub(marginFreedByClosing); @@ -2468,7 +2488,8 @@ export class User { this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount( targetMarketIndex, freeCollateralAfterClose, - ZERO + ZERO, + currentPosition.maxMarginRatio ); oppositeSideTradeSize = perpLiabilityValue; tradeSize = buyingPowerAfterClose; diff --git a/sdk/src/userMap/revenueShareEscrowMap.ts b/sdk/src/userMap/revenueShareEscrowMap.ts new file mode 100644 index 0000000000..fb56628a23 --- /dev/null +++ b/sdk/src/userMap/revenueShareEscrowMap.ts @@ -0,0 +1,306 @@ +import { PublicKey, RpcResponseAndContext } from '@solana/web3.js'; +import { DriftClient } from '../driftClient'; +import { RevenueShareEscrowAccount } from '../types'; +import { getRevenueShareEscrowAccountPublicKey } from '../addresses/pda'; +import { getRevenueShareEscrowFilter } from '../memcmp'; + +export class RevenueShareEscrowMap { + /** + * map from authority pubkey to RevenueShareEscrow account data. + */ + private authorityEscrowMap = new Map(); + private driftClient: DriftClient; + private parallelSync: boolean; + + private fetchPromise?: Promise; + private fetchPromiseResolver: () => void; + + /** + * Creates a new RevenueShareEscrowMap instance. + * + * @param {DriftClient} driftClient - The DriftClient instance. + * @param {boolean} parallelSync - Whether to sync accounts in parallel. + */ + constructor(driftClient: DriftClient, parallelSync?: boolean) { + this.driftClient = driftClient; + this.parallelSync = parallelSync !== undefined ? parallelSync : true; + } + + /** + * Subscribe to all RevenueShareEscrow accounts. + */ + public async subscribe() { + if (this.size() > 0) { + return; + } + + await this.driftClient.subscribe(); + await this.sync(); + } + + public has(authorityPublicKey: string): boolean { + return this.authorityEscrowMap.has(authorityPublicKey); + } + + public get( + authorityPublicKey: string + ): RevenueShareEscrowAccount | undefined { + return this.authorityEscrowMap.get(authorityPublicKey); + } + + /** + * Enforce that a RevenueShareEscrow will exist for the given authorityPublicKey, + * reading one from the blockchain if necessary. + * @param authorityPublicKey + * @returns + */ + public async mustGet( + authorityPublicKey: string + ): Promise { + if (!this.has(authorityPublicKey)) { + await this.addRevenueShareEscrow(authorityPublicKey); + } + return this.get(authorityPublicKey); + } + + public async addRevenueShareEscrow(authority: string) { + const escrowAccountPublicKey = getRevenueShareEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ); + + try { + const accountInfo = await this.driftClient.connection.getAccountInfo( + escrowAccountPublicKey, + 'processed' + ); + + if (accountInfo && accountInfo.data) { + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + accountInfo.data + ) as RevenueShareEscrowAccount; + + this.authorityEscrowMap.set(authority, escrow); + } + } catch (error) { + // RevenueShareEscrow account doesn't exist for this authority, which is normal + console.debug( + `No RevenueShareEscrow account found for authority: ${authority}` + ); + } + } + + public size(): number { + return this.authorityEscrowMap.size; + } + + public async sync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + + this.fetchPromise = new Promise((resolver) => { + this.fetchPromiseResolver = resolver; + }); + + try { + await this.syncAll(); + } finally { + this.fetchPromiseResolver(); + this.fetchPromise = undefined; + } + } + + /** + * A slow, bankrun test friendly version of sync(), uses getAccountInfo on every cached account to refresh data + * @returns + */ + public async slowSync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + for (const authority of this.authorityEscrowMap.keys()) { + const accountInfo = await this.driftClient.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ), + 'confirmed' + ); + const escrowNew = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + accountInfo.data + ) as RevenueShareEscrowAccount; + this.authorityEscrowMap.set(authority, escrowNew); + } + } + + public async syncAll(): Promise { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.driftClient.opts.commitment, + filters: [getRevenueShareEscrowFilter()], + encoding: 'base64', + withContext: true, + }, + ]; + + const rpcJSONResponse: any = + // @ts-ignore + await this.driftClient.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ + pubkey: string; + account: { + data: [string, string]; + }; + }> + > = rpcJSONResponse.result; + + const batchSize = 100; + for (let i = 0; i < rpcResponseAndContext.value.length; i += batchSize) { + const batch = rpcResponseAndContext.value.slice(i, i + batchSize); + + if (this.parallelSync) { + await Promise.all( + batch.map(async (programAccount) => { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + buffer + ) as RevenueShareEscrowAccount; + + // Extract authority from the account data + const authorityKey = escrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, escrow); + } catch (error) { + console.warn( + `Failed to decode RevenueShareEscrow account ${programAccount.pubkey}:`, + error + ); + } + }) + ); + } else { + for (const programAccount of batch) { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + buffer + ) as RevenueShareEscrowAccount; + + // Extract authority from the account data + const authorityKey = escrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, escrow); + } catch (error) { + console.warn( + `Failed to decode RevenueShareEscrow account ${programAccount.pubkey}:`, + error + ); + } + } + } + + // Add a small delay between batches to avoid overwhelming the RPC + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + + /** + * Get all RevenueShareEscrow accounts + */ + public getAll(): Map { + return new Map(this.authorityEscrowMap); + } + + /** + * Get all authorities that have RevenueShareEscrow accounts + */ + public getAuthorities(): string[] { + return Array.from(this.authorityEscrowMap.keys()); + } + + /** + * Get RevenueShareEscrow accounts that have approved referrers + */ + public getEscrowsWithApprovedReferrers(): Map< + string, + RevenueShareEscrowAccount + > { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.approvedBuilders && escrow.approvedBuilders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get RevenueShareEscrow accounts that have active orders + */ + public getEscrowsWithOrders(): Map { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.orders && escrow.orders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get RevenueShareEscrow account by referrer + */ + public getByReferrer( + referrerPublicKey: string + ): RevenueShareEscrowAccount | undefined { + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + return escrow; + } + } + return undefined; + } + + /** + * Get all RevenueShareEscrow accounts for a specific referrer + */ + public getAllByReferrer( + referrerPublicKey: string + ): RevenueShareEscrowAccount[] { + const result: RevenueShareEscrowAccount[] = []; + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + result.push(escrow); + } + } + return result; + } + + public async unsubscribe() { + this.authorityEscrowMap.clear(); + } +} diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d1b68abe8c..d682f5e757 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -44,6 +44,7 @@ export const mockPerpPosition: PerpPosition = { lastBaseAssetAmountPerLp: new BN(0), lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, + maxMarginRatio: 1, }; export const mockAMM: AMM = { diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index fc7f0ace2c..b7c74b140f 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -22,6 +22,7 @@ test_files=( # updateK.ts # postOnlyAmmFulfillment.ts # TODO BROKEN ^^ + builderCodes.ts decodeUser.ts fuel.ts fuelSweep.ts diff --git a/test-scripts/run-til-failure.sh b/test-scripts/run-til-failure.sh new file mode 100644 index 0000000000..d832743171 --- /dev/null +++ b/test-scripts/run-til-failure.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +count=0 +trap 'echo -e "\nStopped after $count runs"; exit 0' INT + +while true; do + if ! bash test-scripts/single-anchor-test.sh --skip-build; then + echo "Test failed after $count successful runs!" + exit 1 + fi + count=$((count + 1)) + echo "Test passed ($count), running again..." +done diff --git a/tests/builderCodes.ts b/tests/builderCodes.ts new file mode 100644 index 0000000000..4f26cd0476 --- /dev/null +++ b/tests/builderCodes.ts @@ -0,0 +1,1612 @@ +import * as anchor from '@coral-xyz/anchor'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; + +import { + TestClient, + OracleSource, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + assert, + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, + RevenueShareAccount, + RevenueShareEscrowAccount, + BASE_PRECISION, + BN, + PRICE_PRECISION, + getMarketOrderParams, + PositionDirection, + PostOnlyParams, + MarketType, + OrderParams, + PEG_PRECISION, + ZERO, + isVariant, + hasBuilder, + parseLogs, + RevenueShareEscrowMap, + getTokenAmount, + RevenueShareSettleRecord, + getLimitOrderParams, + SignedMsgOrderParamsMessage, + QUOTE_PRECISION, +} from '../sdk/src'; + +import { + createUserWithUSDCAccount, + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + printTxLogs, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_STORAGE_DATA } from './pythLazerData'; +import { nanoid } from 'nanoid'; +import { + isBuilderOrderCompleted, + isBuilderOrderReferral, +} from '../sdk/src/math/builder'; +import { createTransferInstruction } from '@solana/spl-token'; + +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +function buildMsg( + marketIndex: number, + baseAssetAmount: BN, + userOrderId: number, + feeBps: number, + slot: BN +) { + const params = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + return { + signedMsgOrderParams: params, + subAccountId: 0, + slot, + uuid: Uint8Array.from(Buffer.from(nanoid(8))), + builderIdx: 0, + builderFeeTenthBps: feeBps, + takeProfitOrderParams: null, + stopLossOrderParams: null, + } as SignedMsgOrderParamsMessage; +} + +describe('builder codes', () => { + const chProgram = anchor.workspace.Drift as Program; + + let usdcMint: Keypair; + + let builderClient: TestClient; + let builderUSDCAccount: Keypair = null; + + let makerClient: TestClient; + let makerUSDCAccount: PublicKey = null; + + let userUSDCAccount: PublicKey = null; + let userClient: TestClient; + + // user without RevenueShareEscrow + let user2USDCAccount: PublicKey = null; + let user2Client: TestClient; + + let escrowMap: RevenueShareEscrowMap; + let bulkAccountLoader: TestBulkAccountLoader; + let bankrunContextWrapper: BankrunContextWrapper; + + let solUsd: PublicKey; + let marketIndexes; + let spotMarketIndexes; + let oracleInfos; + + const usdcAmount = new BN(10000 * 10 ** 6); + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 224.3); + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + marketIndexes = [0, 1]; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solUsd, source: OracleSource.PYTH }]; + + builderClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + subAccountIds: [], + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await builderClient.initialize(usdcMint.publicKey, true); + await builderClient.subscribe(); + + await builderClient.updateFeatureBitFlagsBuilderCodes(true); + // await builderClient.updateFeatureBitFlagsBuilderReferral(true); + + await initializeQuoteSpotMarket(builderClient, usdcMint.publicKey); + + const periodicity = new BN(0); + await builderClient.initializePerpMarket( + 0, + solUsd, + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await builderClient.initializePerpMarket( + 1, + solUsd, + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + builderUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount.add(new BN(1e9).mul(QUOTE_PRECISION)), + bankrunContextWrapper, + builderClient.wallet.publicKey + ); + await builderClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + builderUSDCAccount.publicKey + ); + + // top up pnl pool for mkt 0 and mkt 1 + const spotMarket = builderClient.getSpotMarketAccount(0); + const pnlPoolTopupAmount = new BN(500).mul(QUOTE_PRECISION); + + const transferIx0 = createTransferInstruction( + builderUSDCAccount.publicKey, + spotMarket.vault, + builderClient.wallet.publicKey, + pnlPoolTopupAmount.toNumber() + ); + const tx0 = new Transaction().add(transferIx0); + tx0.recentBlockhash = ( + await bankrunContextWrapper.connection.getLatestBlockhash() + ).blockhash; + tx0.sign(builderClient.wallet.payer); + await bankrunContextWrapper.connection.sendTransaction(tx0); + + // top up pnl pool for mkt 1 + const transferIx1 = createTransferInstruction( + builderUSDCAccount.publicKey, + spotMarket.vault, + builderClient.wallet.publicKey, + pnlPoolTopupAmount.toNumber() + ); + const tx1 = new Transaction().add(transferIx1); + tx1.recentBlockhash = ( + await bankrunContextWrapper.connection.getLatestBlockhash() + ).blockhash; + tx1.sign(builderClient.wallet.payer); + await bankrunContextWrapper.connection.sendTransaction(tx1); + + await builderClient.updatePerpMarketPnlPool(0, pnlPoolTopupAmount); + await builderClient.updatePerpMarketPnlPool(1, pnlPoolTopupAmount); + + // await builderClient.depositIntoPerpMarketFeePool( + // 0, + // new BN(1e6).mul(QUOTE_PRECISION), + // builderUSDCAccount.publicKey + // ); + + [userClient, userUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await userClient.deposit( + usdcAmount, + 0, + userUSDCAccount, + undefined, + false, + undefined, + true + ); + + [user2Client, user2USDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await user2Client.deposit( + usdcAmount, + 0, + user2USDCAccount, + undefined, + false, + undefined, + true + ); + + [makerClient, makerUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + await makerClient.deposit( + usdcAmount, + 0, + makerUSDCAccount, + undefined, + false, + undefined, + true + ); + + escrowMap = new RevenueShareEscrowMap(userClient, false); + }); + + after(async () => { + await builderClient.unsubscribe(); + await userClient.unsubscribe(); + await user2Client.unsubscribe(); + await makerClient.unsubscribe(); + }); + + it('builder can create builder', async () => { + await builderClient.initializeRevenueShare(builderClient.wallet.publicKey); + + const builderAccountInfo = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + + const builderAcc: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfo.data + ); + assert( + builderAcc.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + assert(builderAcc.totalBuilderRewards.toNumber() === 0); + assert(builderAcc.totalReferrerRewards.toNumber() === 0); + }); + + it('user can initialize a RevenueShareEscrow', async () => { + const numOrders = 2; + + // Test the instruction creation + const ix = await userClient.getInitializeRevenueShareEscrowIx( + userClient.wallet.publicKey, + numOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.initializeRevenueShareEscrow( + userClient.wallet.publicKey, + numOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert(accountInfo !== null, 'RevenueShareEscrow account should exist'); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: RevenueShareEscrowAccount = + builderClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + // assert( + // revShareEscrow.referrer.toBase58() === + // builderClient.wallet.publicKey.toBase58() + // ); + assert(revShareEscrow.orders.length === numOrders); + assert(revShareEscrow.approvedBuilders.length === 0); + }); + + it('user can resize RevenueShareEscrow account', async () => { + const newNumOrders = 10; + + // Test the instruction creation + const ix = await userClient.getResizeRevenueShareEscrowOrdersIx( + userClient.wallet.publicKey, + newNumOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.resizeRevenueShareEscrowOrders( + userClient.wallet.publicKey, + newNumOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert( + accountInfo !== null, + 'RevenueShareEscrow account should exist after resize' + ); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: RevenueShareEscrowAccount = + builderClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + // assert( + // revShareEscrow.referrer.toBase58() === + // builderClient.wallet.publicKey.toBase58() + // ); + assert(revShareEscrow.orders.length === newNumOrders); + }); + + it('user can add/update/remove approved builder from RevenueShareEscrow', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; // 1.5% + + // First add a builder + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + true // add + ); + + // Verify the builder was added + let accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + let revShareEscrow: RevenueShareEscrowAccount = + userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const addedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + addedBuilder !== undefined, + 'Builder should be in approved builders list before removal' + ); + assert( + revShareEscrow.approvedBuilders.length === 1, + 'Approved builders list should contain 1 builder' + ); + assert( + addedBuilder.maxFeeTenthBps === maxFeeBps, + 'Builder should have correct max fee bps before removal' + ); + + // update the user fee + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps * 2, + true // update existing builder + ); + + // Verify the builder was updated + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const updatedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + updatedBuilder !== undefined, + 'Builder should be in approved builders list after update' + ); + assert( + updatedBuilder.maxFeeTenthBps === maxFeeBps * 2, + 'Builder should have correct max fee bps after update' + ); + + // Now remove the builder + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + false // remove + ); + + // Verify the builder was removed + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const removedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + removedBuilder.maxFeeTenthBps === 0, + 'Builder should have 0 max fee bps after removal' + ); + }); + + it('user with no RevenueShareEscrow can place and fill order with no builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + let userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: null, + builderFeeTenthBps: null, + }; + + const signedOrderParams = user2Client.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await user2Client.getUserAccountPublicKey(), + takerUserAccount: user2Client.getUserAccount(), + takerStats: user2Client.getUserStatsAccountPublicKey(), + signingAuthority: user2Client.wallet.publicKey, + }, + undefined, + 2 + ); + + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === false); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === false); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === false); + + await user2Client.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await user2Client.getUserAccountPublicKey(), + user2Client.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee'] as BN | null; + const takerFee = events[0].data['takerFee'] as BN; + const totalFeePaid = takerFee; + const referrerReward = new BN(events[0].data['referrerReward'] as number); + assert(builderFee === null); + assert(referrerReward.gt(ZERO)); + + await user2Client.fetchAccounts(); + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + await bankrunContextWrapper.moveTimeForward(100); + + // cancel remaining orders + await user2Client.cancelOrders(); + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = user2Client.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(totalFeePaid).neg()) + ); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert(builderUsdcAfterSettle.eq(builderUsdcBeforeSettle)); + }); + + it('user can place and fill order with builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + // approve builder again + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; // 1.5% + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + true // update existing builder + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + // Should fail if we try first without encoding properly + + let userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const builderFeeBps = 7 * 10; + const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: 0, + builderFeeTenthBps: builderFeeBps, + }; + + const signedOrderParams = userClient.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.fetchAccounts(); + + // try to revoke builder with open orders + try { + await userClient.changeApprovedBuilder( + builder.publicKey, + 0, + false // remove + ); + assert( + false, + 'should throw error when revoking builder with open orders' + ); + } catch (e) { + assert(e.message.includes('0x18b3')); // CannotRevokeBuilderWithOpenOrders + } + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === true); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === true); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === true); + + await escrowMap.slowSync(); + let escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + + // check the corresponding revShareEscrow orders are added + for (let i = 0; i < userOrders.length; i++) { + assert(escrow.orders[i]!.builderIdx === 0); + assert(escrow.orders[i]!.feesAccrued.eq(ZERO)); + assert( + escrow.orders[i]!.feeTenthBps === builderFeeBps, + `builderFeeBps ${escrow.orders[i]!.feeTenthBps} !== ${builderFeeBps}` + ); + assert( + escrow.orders[i]!.orderId === i + 1, + `orderId ${i} is ${escrow.orders[i]!.orderId}` + ); + assert(isVariant(escrow.orders[i]!.marketType, 'perp')); + assert(escrow.orders[i]!.marketIndex === marketIndex); + } + + assert(escrow.approvedBuilders[0]!.authority.equals(builder.publicKey)); + assert(escrow.approvedBuilders[0]!.maxFeeTenthBps === maxFeeBps); + + await userClient.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee'] as BN; + const takerFee = events[0].data['takerFee'] as BN; + // const referrerReward = events[0].data['referrerReward'] as number; + assert( + builderFee.eq(fillQuoteAssetAmount.muln(builderFeeBps).divn(100000)) + ); + + await userClient.fetchAccounts(); + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + const pos = userClient.getUser().getPerpPosition(0); + const takerOrderCumulativeQuoteAssetAmountFilled = events[0].data[ + 'takerOrderCumulativeQuoteAssetAmountFilled' + ] as BN; + assert( + pos.quoteEntryAmount.abs().eq(takerOrderCumulativeQuoteAssetAmountFilled), + `pos.quoteEntryAmount ${pos.quoteEntryAmount.toNumber()} !== takerOrderCumulativeQuoteAssetAmountFilled ${takerOrderCumulativeQuoteAssetAmountFilled.toNumber()}` + ); + + const builderFeePaidBps = + (builderFee.toNumber() / Math.abs(pos.quoteEntryAmount.toNumber())) * + 10_000; + assert( + Math.round(builderFeePaidBps) === builderFeeBps / 10, + `builderFeePaidBps ${builderFeePaidBps} !== builderFeeBps ${ + builderFeeBps / 10 + }` + ); + + // expect 9.5 bps (taker fee - discount) + 7 bps (builder fee) + const takerFeePaidBps = + (takerFee.toNumber() / Math.abs(pos.quoteEntryAmount.toNumber())) * + 10_000; + assert( + Math.round(takerFeePaidBps * 10) === 165, + `takerFeePaidBps ${takerFeePaidBps} !== 16.5 bps` + ); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + assert(escrow.orders[2].orderId === 3); + assert(escrow.orders[2].feesAccrued.gt(ZERO)); + assert(isBuilderOrderCompleted(escrow.orders[2])); + + // cancel remaining orders + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = userClient.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(takerFee).neg()) + ); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + assert(escrow.orders[2].bitFlags === 3); + assert(escrow.orders[2].feesAccrued.eq(builderFee)); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + + const settleLogs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + const settleEvents = parseLogs(builderClient.program, settleLogs); + const builderSettleEvents = settleEvents + .filter((e) => e.name === 'RevenueShareSettleRecord') + .map((e) => e.data) as RevenueShareSettleRecord[]; + + assert(builderSettleEvents.length === 1); + assert(builderSettleEvents[0].builder.equals(builder.publicKey)); + assert(builderSettleEvents[0].referrer == null); + assert(builderSettleEvents[0].feeSettled.eq(builderFee)); + assert(builderSettleEvents[0].marketIndex === marketIndex); + assert(isVariant(builderSettleEvents[0].marketType, 'perp')); + assert(builderSettleEvents[0].builderTotalReferrerRewards.eq(ZERO)); + assert(builderSettleEvents[0].builderTotalBuilderRewards.eq(builderFee)); + + // assert(builderSettleEvents[1].builder === null); + // assert(builderSettleEvents[1].referrer.equals(builder.publicKey)); + // assert(builderSettleEvents[1].feeSettled.eq(new BN(referrerReward))); + // assert(builderSettleEvents[1].marketIndex === marketIndex); + // assert(isVariant(builderSettleEvents[1].marketType, 'spot')); + // assert( + // builderSettleEvents[1].builderTotalReferrerRewards.eq( + // new BN(referrerReward) + // ) + // ); + // assert(builderSettleEvents[1].builderTotalBuilderRewards.eq(builderFee)); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrow.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + const finalBuilderFee = builderUsdcAfterSettle.sub(builderUsdcBeforeSettle); + // .sub(new BN(referrerReward)) + assert( + finalBuilderFee.eq(builderFee), + `finalBuilderFee ${finalBuilderFee.toString()} !== builderFee ${builderFee.toString()}` + ); + }); + + it('user can place and cancel with no fill (no fees accrued, escrow unchanged)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + await escrowMap.slowSync(); + const beforeEscrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const beforeTotalFees = beforeEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const orderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 7, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + const builderFeeBps = 5; + const msg: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: orderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: null, + stopLossOrderParams: null, + builderIdx: 0, + builderFeeTenthBps: builderFeeBps, + }; + + const signed = userClient.signSignedMsgOrderParamsMessage(msg, false); + await builderClient.placeSignedMsgTakerOrder( + signed, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + assert(userClient.getUser().getOpenOrders().length === 0); + + await escrowMap.slowSync(); + const afterEscrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const afterTotalFees = afterEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + assert(afterTotalFees.eq(beforeTotalFees)); + }); + + it('user can place and fill multiple orders (fees accumulate and settle)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + await escrowMap.slowSync(); + const escrowStart = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesInEscrowStart = escrowStart.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + const feeBpsB = 9; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 10, feeBpsA, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const signedB = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 11, feeBpsB, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedB, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + // Fill both orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.data['builderFee'] as BN; + // const referrerRewardA = new BN(fillEventA.data['referrerReward'] as number); + + const fillTxB = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[1].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsB = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxB + ); + const eventsB = parseLogs(builderClient.program, logsB); + const fillEventB = eventsB.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventB !== undefined); + const builderFeeB = fillEventB.data['builderFee'] as BN; + // const referrerRewardB = new BN(fillEventB.data['referrerReward'] as number); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + const escrowAfterFills = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesAccrued = escrowAfterFills.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + const expectedTotal = builderFeeA.add(builderFeeB); + // .add(referrerRewardA) + // .add(referrerRewardB); + assert( + totalFeesAccrued.sub(totalFeesInEscrowStart).eq(expectedTotal), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + + await escrowMap.slowSync(); + const escrowAfterSettle = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + const usdcDiff = builderUsdcAfter.sub(builderUsdcBefore); + assert( + usdcDiff.eq(expectedTotal), + `usdcDiff: ${usdcDiff.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + }); + + it('user can place and fill with multiple maker orders', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + const builderAccountInfoBefore = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + const builderAccBefore: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfoBefore.data + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + // place maker orders + await makerClient.placeOrders([ + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223000000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223500000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + ]); + await makerClient.fetchAccounts(); + const makerOrders = makerClient.getUser().getOpenOrders(); + assert(makerOrders.length === 2); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 10, feeBpsA, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 1); + + // Fill taker against maker orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + { + maker: await makerClient.getUserAccountPublicKey(), + makerStats: makerClient.getUserStatsAccountPublicKey(), + makerUserAccount: makerClient.getUserAccount(), + // order?: Order; + }, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.filter((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.reduce( + (sum, e) => sum.add(e.data['builderFee'] as BN), + ZERO + ); + // const referrerRewardA = fillEventA.reduce( + // (sum, e) => sum.add(new BN(e.data['referrerReward'] as number)), + // ZERO + // ); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + const escrowAfterFills = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesAccrued = escrowAfterFills.orders + .filter((o) => !isBuilderOrderReferral(o)) + .reduce((sum, o) => sum.add(o.feesAccrued ?? ZERO), ZERO); + assert( + totalFeesAccrued.eq(builderFeeA), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, builderFeeA: ${builderFeeA.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + + await escrowMap.slowSync(); + const escrowAfterSettle = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert( + builderUsdcAfter.sub(builderUsdcBefore).eq(builderFeeA), + // .add(referrerRewardA) + `builderUsdcAfter: ${builderUsdcAfter.toString()} !== builderUsdcBefore ${builderUsdcBefore.toString()} + builderFeeA ${builderFeeA.toString()}` + ); + + const builderAccountInfoAfter = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + const builderAccAfter: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfoAfter.data + ); + assert( + builderAccAfter.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + + const builderFeeChange = builderAccAfter.totalBuilderRewards.sub( + builderAccBefore.totalBuilderRewards + ); + assert( + builderFeeChange.eq(builderFeeA), + `builderFeeChange: ${builderFeeChange.toString()}, builderFeeA: ${builderFeeA.toString()}` + ); + + // const referrerRewardChange = builderAccAfter.totalReferrerRewards.sub( + // builderAccBefore.totalReferrerRewards + // ); + // assert(referrerRewardChange.eq(referrerRewardA)); + }); + + // it('can track referral rewards for 2 markets', async () => { + // const builderAccountInfoBefore = + // await bankrunContextWrapper.connection.getAccountInfo( + // getRevenueShareAccountPublicKey( + // builderClient.program.programId, + // builderClient.wallet.publicKey + // ) + // ); + // const builderAccBefore: RevenueShareAccount = + // builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + // 'RevenueShare', + // builderAccountInfoBefore.data + // ); + // // await escrowMap.slowSync(); + // // const escrowBeforeFills = (await escrowMap.mustGet( + // // userClient.wallet.publicKey.toBase58() + // // )) as RevenueShareEscrowAccount; + + // const slot = new BN( + // await bankrunContextWrapper.connection.toConnection().getSlot() + // ); + + // // place 2 orders in different markets + + // const signedA = userClient.signSignedMsgOrderParamsMessage( + // buildMsg(0, BASE_PRECISION, 1, 5, slot), + // false + // ); + // await builderClient.placeSignedMsgTakerOrder( + // signedA, + // 0, + // { + // taker: await userClient.getUserAccountPublicKey(), + // takerUserAccount: userClient.getUserAccount(), + // takerStats: userClient.getUserStatsAccountPublicKey(), + // signingAuthority: userClient.wallet.publicKey, + // }, + // undefined, + // 2 + // ); + + // const signedB = userClient.signSignedMsgOrderParamsMessage( + // buildMsg(1, BASE_PRECISION, 2, 5, slot), + // false + // ); + // await builderClient.placeSignedMsgTakerOrder( + // signedB, + // 1, + // { + // taker: await userClient.getUserAccountPublicKey(), + // takerUserAccount: userClient.getUserAccount(), + // takerStats: userClient.getUserStatsAccountPublicKey(), + // signingAuthority: userClient.wallet.publicKey, + // }, + // undefined, + // 2 + // ); + + // await userClient.fetchAccounts(); + // const openOrders = userClient.getUser().getOpenOrders(); + + // const fillTxA = await makerClient.fillPerpOrder( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // { + // marketIndex: 0, + // orderId: openOrders.find( + // (o) => isVariant(o.status, 'open') && o.marketIndex === 0 + // )!.orderId, + // }, + // undefined, + // { + // referrer: await builderClient.getUserAccountPublicKey(), + // referrerStats: builderClient.getUserStatsAccountPublicKey(), + // }, + // undefined, + // undefined, + // undefined, + // true + // ); + // const logsA = await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // fillTxA + // ); + // const eventsA = parseLogs(builderClient.program, logsA); + // const fillsA = eventsA.filter((e) => e.name === 'OrderActionRecord'); + // const fillAReferrerReward = fillsA[0]['data']['referrerReward'] as number; + // assert(fillsA.length > 0); + // // debug: fillsA[0]['data'] + + // const fillTxB = await makerClient.fillPerpOrder( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // { + // marketIndex: 1, + // orderId: openOrders.find( + // (o) => isVariant(o.status, 'open') && o.marketIndex === 1 + // )!.orderId, + // }, + // undefined, + // { + // referrer: await builderClient.getUserAccountPublicKey(), + // referrerStats: builderClient.getUserStatsAccountPublicKey(), + // }, + // undefined, + // undefined, + // undefined, + // true + // ); + // const logsB = await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // fillTxB + // ); + // const eventsB = parseLogs(builderClient.program, logsB); + // const fillsB = eventsB.filter((e) => e.name === 'OrderActionRecord'); + // assert(fillsB.length > 0); + // const fillBReferrerReward = fillsB[0]['data']['referrerReward'] as number; + // // debug: fillsB[0]['data'] + + // await escrowMap.slowSync(); + // const escrowAfterFills = (await escrowMap.mustGet( + // userClient.wallet.publicKey.toBase58() + // )) as RevenueShareEscrowAccount; + + // const referrerOrdersMarket0 = escrowAfterFills.orders.filter( + // (o) => o.marketIndex === 0 && isBuilderOrderReferral(o) + // ); + // const referrerOrdersMarket1 = escrowAfterFills.orders.filter( + // (o) => o.marketIndex === 1 && isBuilderOrderReferral(o) + // ); + // assert(referrerOrdersMarket0[0].marketIndex === 0); + // assert( + // referrerOrdersMarket0[0].feesAccrued.eq(new BN(fillAReferrerReward)) + // ); + // assert(referrerOrdersMarket1[0].marketIndex === 1); + // assert( + // referrerOrdersMarket1[0].feesAccrued.eq(new BN(fillBReferrerReward)) + // ); + + // // settle pnl + // const settleTxA = await builderClient.settleMultiplePNLs( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // [0, 1], + // SettlePnlMode.MUST_SETTLE, + // escrowMap + // ); + // await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // settleTxA + // ); + + // await escrowMap.slowSync(); + // const escrowAfterSettle = (await escrowMap.mustGet( + // userClient.wallet.publicKey.toBase58() + // )) as RevenueShareEscrowAccount; + // const referrerOrdersMarket0AfterSettle = escrowAfterSettle.orders.filter( + // (o) => o.marketIndex === 0 && isBuilderOrderReferral(o) + // ); + // const referrerOrdersMarket1AfterSettle = escrowAfterSettle.orders.filter( + // (o) => o.marketIndex === 1 && isBuilderOrderReferral(o) + // ); + // assert(referrerOrdersMarket0AfterSettle.length === 1); + // assert(referrerOrdersMarket1AfterSettle.length === 1); + // assert(referrerOrdersMarket0AfterSettle[0].feesAccrued.eq(ZERO)); + // assert(referrerOrdersMarket1AfterSettle[0].feesAccrued.eq(ZERO)); + + // const builderAccountInfoAfter = + // await bankrunContextWrapper.connection.getAccountInfo( + // getRevenueShareAccountPublicKey( + // builderClient.program.programId, + // builderClient.wallet.publicKey + // ) + // ); + // const builderAccAfter: RevenueShareAccount = + // builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + // 'RevenueShare', + // builderAccountInfoAfter.data + // ); + // const referrerRewards = builderAccAfter.totalReferrerRewards.sub( + // builderAccBefore.totalReferrerRewards + // ); + // assert( + // referrerRewards.eq(new BN(fillAReferrerReward + fillBReferrerReward)) + // ); + // }); +}); diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 0424df1d6b..8ba2417fd7 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -583,7 +583,7 @@ describe('LP Pool', () => { expect.fail('should have failed'); } catch (e) { console.log(e.message); - expect(e.message).to.contain('0x18ae'); + expect(e.message).to.contain('0x18b6'); // InvalidAmmConstituentMappingArgument } // Bad constituent index @@ -597,7 +597,7 @@ describe('LP Pool', () => { ]); expect.fail('should have failed'); } catch (e) { - expect(e.message).to.contain('0x18ae'); + expect(e.message).to.contain('0x18b6'); // InvalidAmmConstituentMappingArgument } }); @@ -614,7 +614,7 @@ describe('LP Pool', () => { }); expect.fail('should have failed'); } catch (e) { - assert(e.message.includes('0x18b7')); + assert(e.message.includes('0x18bf')); // LpPoolAumDelayed } }); @@ -640,7 +640,7 @@ describe('LP Pool', () => { await adminClient.sendTransaction(tx); } catch (e) { console.log(e.message); - assert(e.message.includes('0x18c0')); + assert(e.message.includes('0x18c8')); // InvalidConstituentOperation } await adminClient.updateConstituentPausedOperations( getConstituentPublicKey(program.programId, lpPoolKey, 0), @@ -699,7 +699,7 @@ describe('LP Pool', () => { await adminClient.updateLpPoolAum(lpPool, [0]); expect.fail('should have failed'); } catch (e) { - assert(e.message.includes('0x18b3')); + assert(e.message.includes('0x18bb')); // WrongNumberOfConstituents } }); @@ -1582,7 +1582,7 @@ describe('LP Pool', () => { await adminClient.settlePerpToLpPool(encodeName(lpPoolName), [0, 1, 2]); assert(false, 'Should have thrown'); } catch (e) { - assert(e.message.includes('0x18bd')); + assert(e.message.includes('0x18c5')); // SettleLpPoolDisabled } await adminClient.updateFeatureBitFlagsSettleLpPool(true); diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 7cfc3e0c87..764dd941ca 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -589,7 +589,7 @@ describe('LP Pool', () => { try { await adminClient.sendTransaction(tx); } catch (e) { - assert(e.message.includes('0x18c0')); + assert(e.message.includes('0x18c8')); // InvalidConstituentOperation } await adminClient.updateConstituentStatus( c0.pubkey, diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 3b23722445..5971501251 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -1564,7 +1564,7 @@ describe('place and make signedMsg order', () => { ); assert.fail('should fail'); } catch (e) { - assert(e.toString().includes('0x1776')); + assert(e.toString().includes('Error: Invalid option')); const takerOrders = takerDriftClient.getUser().getOpenOrders(); assert(takerOrders.length == 0); } diff --git a/tests/subaccounts.ts b/tests/subaccounts.ts index 2f6f5c5cd2..fe0c63acc4 100644 --- a/tests/subaccounts.ts +++ b/tests/subaccounts.ts @@ -158,6 +158,7 @@ describe('subaccounts', () => { undefined, donationAmount ); + await driftClient.fetchAccounts(); await driftClient.addUser(1); await driftClient.switchActiveUser(1); diff --git a/tests/switchboardTxCus.ts b/tests/switchboardTxCus.ts index 40e6a33277..b3a933eb18 100644 --- a/tests/switchboardTxCus.ts +++ b/tests/switchboardTxCus.ts @@ -219,6 +219,6 @@ describe('switchboard place orders cus', () => { const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); - assert(cus < 410000); + assert(cus < 413000); }); }); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index 802c435f43..b4a1ca83a6 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -43,6 +43,7 @@ import { PositionDirection, DriftClient, OrderType, + ReferrerInfo, ConstituentAccount, SpotMarketAccount, } from '../sdk/src'; @@ -403,7 +404,8 @@ export async function initializeAndSubscribeDriftClient( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise { const driftClient = new TestClient({ connection, @@ -428,7 +430,7 @@ export async function initializeAndSubscribeDriftClient( }, }); await driftClient.subscribe(); - await driftClient.initializeUserAccount(); + await driftClient.initializeUserAccount(0, undefined, referrerInfo); return driftClient; } @@ -440,7 +442,8 @@ export async function createUserWithUSDCAccount( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise<[TestClient, PublicKey, Keypair]> { const userKeyPair = await createFundedKeyPair(context); const usdcAccount = await createUSDCAccountForUser( @@ -456,7 +459,8 @@ export async function createUserWithUSDCAccount( marketIndexes, bankIndexes, oracleInfos, - accountLoader + accountLoader, + referrerInfo ); return [driftClient, usdcAccount, userKeyPair]; @@ -559,7 +563,6 @@ export async function printTxLogs( const tx = await connection.getTransaction(txSig, { commitment: 'confirmed', }); - console.log('tx logs', tx.meta.logMessages); return tx.meta.logMessages; } From 53a5009b48b3f969ae256dbcc86ee83582862ba4 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:46:46 -0700 Subject: [PATCH 091/159] borrow lend accounting (#1905) * add in event * emit new event type * event subscriber * reformat event * fix constituent map index * bump consittunet index * cargo tests work --- programs/drift/src/instructions/lp_pool.rs | 68 +++++++++++++++- programs/drift/src/state/constituent_map.rs | 8 +- programs/drift/src/state/events.rs | 18 +++++ programs/drift/src/state/lp_pool.rs | 5 +- programs/drift/src/state/lp_pool/tests.rs | 87 +++++++++++---------- sdk/src/events/types.ts | 4 +- sdk/src/idl/drift.json | 56 +++++++++++++ sdk/src/types.ts | 12 +++ 8 files changed, 208 insertions(+), 50 deletions(-) diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 49f4f4f56a..b4cd8b1c0d 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -2,8 +2,9 @@ use anchor_lang::{prelude::*, Accounts, Key, Result}; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::ids::lp_pool_swap_wallet; -use crate::math::constants::{PERCENTAGE_PRECISION, PRICE_PRECISION_I64}; +use crate::math::constants::PRICE_PRECISION_I64; use crate::math::oracle::OracleValidity; +use crate::state::events::{DepositDirection, LPBorrowLendDepositRecord}; use crate::state::paused_operations::ConstituentLpOperation; use crate::validation::whitelist::validate_whitelist_token; use crate::{ @@ -26,7 +27,7 @@ use crate::{ state::{ amm_cache::{AmmCacheFixed, CacheInfo, AMM_POSITIONS_CACHE}, constituent_map::{ConstituentMap, ConstituentSet}, - events::{emit_stack, LPMintRedeemRecord, LPSettleRecord, LPSwapRecord}, + events::{emit_stack, LPMintRedeemRecord, LPSwapRecord}, lp_pool::{ update_constituent_target_base_for_derivatives, AmmConstituentDatum, AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, @@ -1369,6 +1370,8 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( )?; let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let mut constituent = ctx.accounts.constituent.load_mut()?; + if amount == 0 { return Err(ErrorCode::InsufficientDeposit.into()); } @@ -1382,8 +1385,14 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( Some(&oracle_data), clock.unix_timestamp, )?; + let token_balance_after_cumulative_interest_update = constituent + .spot_balance + .get_signed_token_amount(&spot_market)?; + + let interest_accrued_token_amount = token_balance_after_cumulative_interest_update + .cast::()? + .safe_sub(constituent.last_spot_balance_token_amount)?; - let mut constituent = ctx.accounts.constituent.load_mut()?; if constituent.last_oracle_slot < oracle_data_slot { constituent.last_oracle_price = oracle_data.price; constituent.last_oracle_slot = oracle_data_slot; @@ -1454,6 +1463,27 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( "Constituent balance mismatch after withdraw from program vault" )?; + let new_token_balance = constituent + .spot_balance + .get_signed_token_amount(&spot_market)? + .cast::()?; + + emit!(LPBorrowLendDepositRecord { + ts: clock.unix_timestamp, + slot: clock.slot, + spot_market_index: spot_market.market_index, + constituent_index: constituent.constituent_index, + direction: DepositDirection::Deposit, + token_balance: new_token_balance, + last_token_balance: constituent.last_spot_balance_token_amount, + interest_accrued_token_amount, + amount_deposit_withdraw: amount, + }); + constituent.last_spot_balance_token_amount = new_token_balance; + constituent.cumulative_spot_interest_accrued_token_amount = constituent + .cumulative_spot_interest_accrued_token_amount + .safe_add(interest_accrued_token_amount)?; + Ok(()) } @@ -1474,19 +1504,28 @@ pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( )?; let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let mut constituent = ctx.accounts.constituent.load_mut()?; + if amount == 0 { return Err(ErrorCode::InsufficientDeposit.into()); } let oracle_data = oracle_map.get_price_data(&oracle_id)?; let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + controller::spot_balance::update_spot_market_cumulative_interest( &mut spot_market, Some(&oracle_data), clock.unix_timestamp, )?; + let token_balance_after_cumulative_interest_update = constituent + .spot_balance + .get_signed_token_amount(&spot_market)?; + + let interest_accrued_token_amount = token_balance_after_cumulative_interest_update + .cast::()? + .safe_sub(constituent.last_spot_balance_token_amount)?; - let mut constituent = ctx.accounts.constituent.load_mut()?; if constituent.last_oracle_slot < oracle_data_slot { constituent.last_oracle_price = oracle_data.price; constituent.last_oracle_slot = oracle_data_slot; @@ -1507,6 +1546,27 @@ pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( Some(remaining_accounts), )?; + let new_token_balance = constituent + .spot_balance + .get_signed_token_amount(&spot_market)? + .cast::()?; + + emit!(LPBorrowLendDepositRecord { + ts: clock.unix_timestamp, + slot: clock.slot, + spot_market_index: spot_market.market_index, + constituent_index: constituent.constituent_index, + direction: DepositDirection::Withdraw, + token_balance: new_token_balance, + last_token_balance: constituent.last_spot_balance_token_amount, + interest_accrued_token_amount, + amount_deposit_withdraw: amount, + }); + constituent.last_spot_balance_token_amount = new_token_balance; + constituent.cumulative_spot_interest_accrued_token_amount = constituent + .cumulative_spot_interest_accrued_token_amount + .safe_add(interest_accrued_token_amount)?; + Ok(()) } diff --git a/programs/drift/src/state/constituent_map.rs b/programs/drift/src/state/constituent_map.rs index e14bf7f0c0..57518c0d4b 100644 --- a/programs/drift/src/state/constituent_map.rs +++ b/programs/drift/src/state/constituent_map.rs @@ -132,8 +132,8 @@ impl<'a> ConstituentMap<'a> { "Constituent lp pool pubkey does not match lp pool pubkey" )?; - // constituent index 276 bytes from front of account - let constituent_index = u16::from_le_bytes(*array_ref![data, 292, 2]); + // constituent index 308 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); if constituent_map.0.contains_key(&constituent_index) { msg!( "Can not include same constituent index twice {}", @@ -183,7 +183,7 @@ impl<'a> ConstituentMap<'a> { } // market index 1160 bytes from front of account - let constituent_index = u16::from_le_bytes(*array_ref![data, 292, 2]); + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); let is_writable = account_info.is_writable; let account_loader: AccountLoader = @@ -221,7 +221,7 @@ impl<'a> ConstituentMap<'a> { return Err(ErrorCode::ConstituentCouldNotLoad); } - let constituent_index = u16::from_le_bytes(*array_ref![data, 292, 2]); + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); if constituent_map.0.contains_key(&constituent_index) { msg!( diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 9ebe15e0fd..63811d8b94 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -873,3 +873,21 @@ pub struct LPMintRedeemRecord { impl Size for LPMintRedeemRecord { const SIZE: usize = 328; } + +#[event] +#[derive(Default)] +pub struct LPBorrowLendDepositRecord { + pub ts: i64, + pub slot: u64, + pub spot_market_index: u16, + pub constituent_index: u16, + pub direction: DepositDirection, + pub token_balance: i64, + pub last_token_balance: i64, + pub interest_accrued_token_amount: i64, + pub amount_deposit_withdraw: u64, +} + +impl Size for LPBorrowLendDepositRecord { + const SIZE: usize = 72; +} diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 6b455d7e8b..3bf6896c69 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -785,6 +785,9 @@ pub struct Constituent { /// spot borrow-lend balance for constituent pub spot_balance: ConstituentSpotBalance, // should be in constituent base asset + pub last_spot_balance_token_amount: i64, // token precision + pub cumulative_spot_interest_accrued_token_amount: i64, // token precision + /// max deviation from target_weight allowed for the constituent /// precision: PERCENTAGE_PRECISION pub max_weight_deviation: i64, @@ -844,7 +847,7 @@ pub struct Constituent { } impl Size for Constituent { - const SIZE: usize = 304; + const SIZE: usize = 320; } #[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index 19e72d8a03..8a7638cc24 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -1047,14 +1047,14 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum_6_decimals_large_aum() { get_remove_liquidity_mint_amount_scenario( - 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) - 0, // now - 6, // in_decimals - 100_000_000_000 * 1_000_000, // in_amount - 100_000_000_000 * 1_000_000, // dlp_total_supply - 99990000000000000, // expected_out_amount - 10000000000000, // expected_lp_fee - 349765020000000, // expected_out_fee_amount + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 6, // in_decimals + 100_000_000_000 * 1_000_000 - 1_000_000, // Leave in QUOTE AMOUNT + 100_000_000_000 * 1_000_000, // dlp_total_supply + 99989999900000000, // expected_out_amount + 9999999999900, // expected_lp_fee + 349765019650200, // expected_out_fee_amount 1, 2, 2, @@ -1065,14 +1065,14 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals_large_aum() { get_remove_liquidity_mint_amount_scenario( - 10_000_000_000_000_000, // last_aum ($10,000,000,000) - 0, // now - 8, // in_decimals - 10_000_000_000 * 100_000_000, // in_amount - 10_000_000_000 * 1_000_000, // dlp_total_supply - 9_999_000_000_000_000_0000, // expected_out_amount - 100000000000000, // expected_lp_fee - 3764623500000000000, // expected_out_fee_amount + 10_000_000_000_000_000, // last_aum ($10,000,000,000) + 0, // now + 8, // in_decimals + 10_000_000_000 * 1_000_000 - 1_000_000, // in_amount + 10_000_000_000 * 1_000_000, // dlp_total_supply + 999899999000000000, // expected_out_amount + (10_000_000_000 * 1_000_000 - 1_000_000) / 10000, // expected_lp_fee + 3497650196502000, // expected_out_fee_amount 1, 2, 2, @@ -1540,6 +1540,7 @@ mod swap_fee_tests { #[cfg(test)] mod settle_tests { + use crate::math::constants::{QUOTE_PRECISION, QUOTE_PRECISION_I64, QUOTE_PRECISION_U64}; use crate::math::lp_pool::perp_lp_pool_settlement::{ calculate_settlement_amount, update_cache_info, SettlementContext, SettlementDirection, SettlementResult, @@ -1570,17 +1571,17 @@ mod settle_tests { #[test] fn test_lp_to_perp_settlement_sufficient_balance() { let ctx = SettlementContext { - quote_owed_from_lp: 500, - quote_constituent_token_balance: 1000, - fee_pool_balance: 300, - pnl_pool_balance: 200, + quote_owed_from_lp: 500 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 300 * QUOTE_PRECISION, + pnl_pool_balance: 200 * QUOTE_PRECISION, quote_market: &create_mock_spot_market(), - max_settle_quote_amount: 10000, + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, }; let result = calculate_settlement_amount(&ctx).unwrap(); assert_eq!(result.direction, SettlementDirection::FromLpPool); - assert_eq!(result.amount_transferred, 500); + assert_eq!(result.amount_transferred, 500 * QUOTE_PRECISION_U64); assert_eq!(result.fee_pool_used, 0); assert_eq!(result.pnl_pool_used, 0); } @@ -1588,17 +1589,20 @@ mod settle_tests { #[test] fn test_lp_to_perp_settlement_insufficient_balance() { let ctx = SettlementContext { - quote_owed_from_lp: 1500, - quote_constituent_token_balance: 1000, - fee_pool_balance: 300, - pnl_pool_balance: 200, + quote_owed_from_lp: 1500 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 300 * QUOTE_PRECISION, + pnl_pool_balance: 200 * QUOTE_PRECISION, quote_market: &create_mock_spot_market(), - max_settle_quote_amount: 10000, + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, }; let result = calculate_settlement_amount(&ctx).unwrap(); assert_eq!(result.direction, SettlementDirection::FromLpPool); - assert_eq!(result.amount_transferred, 1000); // Limited by LP balance + assert_eq!( + result.amount_transferred, + 1000 * QUOTE_PRECISION_U64 - QUOTE_PRECISION_U64 + ); // Limited by LP balance } #[test] @@ -1809,17 +1813,20 @@ mod settle_tests { fn test_exact_boundary_settlements() { // Test when quote_owed exactly equals LP balance let ctx = SettlementContext { - quote_owed_from_lp: 1000, - quote_constituent_token_balance: 1000, - fee_pool_balance: 500, - pnl_pool_balance: 300, + quote_owed_from_lp: 1000 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 500 * QUOTE_PRECISION, + pnl_pool_balance: 300 * QUOTE_PRECISION, quote_market: &create_mock_spot_market(), - max_settle_quote_amount: 10000, + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, }; let result = calculate_settlement_amount(&ctx).unwrap(); assert_eq!(result.direction, SettlementDirection::FromLpPool); - assert_eq!(result.amount_transferred, 1000); + assert_eq!( + result.amount_transferred, + 1000 * QUOTE_PRECISION_U64 - QUOTE_PRECISION_U64 + ); // Leave QUOTE PRECISION // Test when negative quote_owed exactly equals total pool balance let ctx = SettlementContext { @@ -1852,7 +1859,7 @@ mod settle_tests { let result = calculate_settlement_amount(&ctx).unwrap(); assert_eq!(result.direction, SettlementDirection::FromLpPool); - assert_eq!(result.amount_transferred, 1); + assert_eq!(result.amount_transferred, 0); // Cannot transfer if less than QUOTE_PRECISION // Test with minimal negative amount let ctx = SettlementContext { @@ -2052,17 +2059,17 @@ mod settle_tests { #[test] fn test_lp_to_perp_capped_with_max() { let ctx = SettlementContext { - quote_owed_from_lp: 1100, - quote_constituent_token_balance: 2000, + quote_owed_from_lp: 1100 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 2000 * QUOTE_PRECISION_U64, fee_pool_balance: 0, // No fee pool - pnl_pool_balance: 1200, + pnl_pool_balance: 1200 * QUOTE_PRECISION, quote_market: &create_mock_spot_market(), - max_settle_quote_amount: 1000, + max_settle_quote_amount: 1000 * QUOTE_PRECISION_U64, }; let result = calculate_settlement_amount(&ctx).unwrap(); assert_eq!(result.direction, SettlementDirection::FromLpPool); - assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.amount_transferred, 1000 * QUOTE_PRECISION_U64); // Leave QUOTE PRECISION in the balance assert_eq!(result.fee_pool_used, 0); assert_eq!(result.pnl_pool_used, 0); } diff --git a/sdk/src/events/types.ts b/sdk/src/events/types.ts index 1b2219f544..4614b7e753 100644 --- a/sdk/src/events/types.ts +++ b/sdk/src/events/types.ts @@ -24,6 +24,7 @@ import { LPMintRedeemRecord, LPSettleRecord, LPSwapRecord, + LPBorrowLendDepositRecord, } from '../types'; import { EventEmitter } from 'events'; @@ -147,7 +148,8 @@ export type DriftEvent = | Event | Event | Event - | Event; + | Event + | Event; export interface EventSubscriberEvents { newEvent: (event: WrappedEvent) => void; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 8914098c0a..3ce273767c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -9910,6 +9910,10 @@ "defined": "ConstituentSpotBalance" } }, + { + "name": "lastSpotBalanceTokenAmount", + "type": "i64" + }, { "name": "maxWeightDeviation", "docs": [ @@ -17857,6 +17861,58 @@ "index": false } ] + }, + { + "name": "LPBorrowLendDepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "slot", + "type": "u64", + "index": false + }, + { + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "constituentIndex", + "type": "u16", + "index": false + }, + { + "name": "direction", + "type": { + "defined": "DepositDirection" + }, + "index": false + }, + { + "name": "tokenBalance", + "type": "i64", + "index": false + }, + { + "name": "lastTokenBalance", + "type": "i64", + "index": false + }, + { + "name": "interestAccruedTokenAmount", + "type": "i64", + "index": false + }, + { + "name": "amountDepositWithdraw", + "type": "u64", + "index": false + } + ] } ], "errors": [ diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 8ddd31a750..3fce8c57df 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -805,6 +805,18 @@ export type LPSettleRecord = { lpPrice: BN; }; +export type LPBorrowLendDepositRecord = { + ts: BN; + slot: BN; + spotMarketIndex: number; + constituentIndex: number; + direction: DepositDirection; + tokenBalance: BN; + lastTokenBalance: BN; + interestAccruedTokenAmount: BN; + amountDepositWithdraw: BN; +}; + export type StateAccount = { admin: PublicKey; exchangeStatus: number; From 88d1dc88ae0e2081040490a305d5e2560308188d Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:46:14 -0700 Subject: [PATCH 092/159] remove duplicate admin client funcs --- sdk/src/adminClient.ts | 128 ----------------------------------------- 1 file changed, 128 deletions(-) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index b9e6f2126c..45da6a96bd 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4862,134 +4862,6 @@ export class AdminClient extends DriftClient { ); } - public async updateFeatureBitFlagsMedianTriggerPrice( - enable: boolean - ): Promise { - const updateFeatureBitFlagsMedianTriggerPriceIx = - await this.getUpdateFeatureBitFlagsMedianTriggerPriceIx(enable); - const tx = await this.buildTransaction( - updateFeatureBitFlagsMedianTriggerPriceIx - ); - const { txSig } = await this.sendTransaction(tx, [], this.opts); - - return txSig; - } - - public async getUpdateFeatureBitFlagsMedianTriggerPriceIx( - enable: boolean - ): Promise { - return await this.program.instruction.updateFeatureBitFlagsMedianTriggerPrice( - enable, - { - accounts: { - admin: this.useHotWalletAdmin - ? this.wallet.publicKey - : this.getStateAccount().admin, - state: await this.getStatePublicKey(), - }, - } - ); - } - - public async updateDelegateUserGovTokenInsuranceStake( - authority: PublicKey, - delegate: PublicKey - ): Promise { - const updateDelegateUserGovTokenInsuranceStakeIx = - await this.getUpdateDelegateUserGovTokenInsuranceStakeIx( - authority, - delegate - ); - - const tx = await this.buildTransaction( - updateDelegateUserGovTokenInsuranceStakeIx - ); - const { txSig } = await this.sendTransaction(tx, [], this.opts); - - return txSig; - } - - public async getUpdateDelegateUserGovTokenInsuranceStakeIx( - authority: PublicKey, - delegate: PublicKey - ): Promise { - const marketIndex = GOV_SPOT_MARKET_INDEX; - const spotMarket = this.getSpotMarketAccount(marketIndex); - const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( - this.program.programId, - delegate, - marketIndex - ); - const userStatsPublicKey = getUserStatsAccountPublicKey( - this.program.programId, - authority - ); - - const ix = - this.program.instruction.getUpdateDelegateUserGovTokenInsuranceStakeIx({ - accounts: { - state: await this.getStatePublicKey(), - spotMarket: spotMarket.pubkey, - insuranceFundStake: ifStakeAccountPublicKey, - userStats: userStatsPublicKey, - signer: this.wallet.publicKey, - insuranceFundVault: spotMarket.insuranceFund.vault, - }, - }); - - return ix; - } - - public async depositIntoInsuranceFundStake( - marketIndex: number, - amount: BN, - userStatsPublicKey: PublicKey, - insuranceFundStakePublicKey: PublicKey, - userTokenAccountPublicKey: PublicKey, - txParams?: TxParams - ): Promise { - const tx = await this.buildTransaction( - await this.getDepositIntoInsuranceFundStakeIx( - marketIndex, - amount, - userStatsPublicKey, - insuranceFundStakePublicKey, - userTokenAccountPublicKey - ), - txParams - ); - const { txSig } = await this.sendTransaction(tx, [], this.opts); - return txSig; - } - - public async getDepositIntoInsuranceFundStakeIx( - marketIndex: number, - amount: BN, - userStatsPublicKey: PublicKey, - insuranceFundStakePublicKey: PublicKey, - userTokenAccountPublicKey: PublicKey - ): Promise { - const spotMarket = this.getSpotMarketAccount(marketIndex); - return await this.program.instruction.depositIntoInsuranceFundStake( - marketIndex, - amount, - { - accounts: { - signer: this.wallet.publicKey, - state: await this.getStatePublicKey(), - spotMarket: spotMarket.pubkey, - insuranceFundStake: insuranceFundStakePublicKey, - userStats: userStatsPublicKey, - spotMarketVault: spotMarket.vault, - insuranceFundVault: spotMarket.insuranceFund.vault, - userTokenAccount: userTokenAccountPublicKey, - tokenProgram: this.getTokenProgramForSpotMarket(spotMarket), - driftSigner: this.getSignerPublicKey(), - }, - } - ); - } - public async updateFeatureBitFlagsBuilderCodes( enable: boolean ): Promise { From 6faf6a28f268e91c6cc8db279627a4589238a11c Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 29 Sep 2025 16:46:33 -0700 Subject: [PATCH 093/159] update idl --- sdk/src/idl/drift.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 3ce273767c..8364ac17c9 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -9914,6 +9914,10 @@ "name": "lastSpotBalanceTokenAmount", "type": "i64" }, + { + "name": "cumulativeSpotInterestAccruedTokenAmount", + "type": "i64" + }, { "name": "maxWeightDeviation", "docs": [ From f1a23d8062e6b2f62ffaef32808a857bee7a2e2a Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:39:58 -0700 Subject: [PATCH 094/159] bump constituent map max size --- sdk/src/constituentMap/constituentMap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts index 59a4d88d22..e4ab519944 100644 --- a/sdk/src/constituentMap/constituentMap.ts +++ b/sdk/src/constituentMap/constituentMap.ts @@ -15,9 +15,9 @@ import { ZSTDDecoder } from 'zstddec'; import { encodeName } from '../userName'; import { getLpPoolPublicKey } from '../addresses/pda'; -const MAX_CONSTITUENT_SIZE_BYTES = 304; // TODO: update this when account is finalized +const MAX_CONSTITUENT_SIZE_BYTES = 320; // TODO: update this when account is finalized -const LP_POOL_NAME = 'test lp pool 2'; +const LP_POOL_NAME = 'test lp pool 3'; export type ConstituentMapConfig = { driftClient: DriftClient; From cec4e6d42c2e71e07ecac54933cd43fc2f73df8f Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 29 Sep 2025 22:35:24 -0700 Subject: [PATCH 095/159] make working devcontainer and dockerfile --- .devcontainer/Dockerfile | 56 +++++++++++++++++++++------------ .devcontainer/devcontainer.json | 44 ++++++++++++++++++++++++-- README.md | 42 +++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 22 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2d4688614c..97523ce216 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,18 +4,29 @@ # is released on GitHub. # -FROM rust:1.75 +FROM --platform=linux/amd64 rust:1.70.0 ARG DEBIAN_FRONTEND=noninteractive -ARG SOLANA_CLI="1.14.7" -ARG ANCHOR_CLI="0.26.0" -ARG NODE_VERSION="v18.16.0" +ARG SOLANA_CLI="1.16.27" +ARG ANCHOR_CLI="0.29.0" +ARG NODE_VERSION="20.18.x" -ENV HOME="/root" +# Create a non-root user +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + && apt-get update \ + && apt-get install -y sudo \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +ENV HOME="/home/$USERNAME" ENV PATH="${HOME}/.cargo/bin:${PATH}" ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" -ENV PATH="${HOME}/.nvm/versions/node/${NODE_VERSION}/bin:${PATH}" # Install base utilities. RUN mkdir -p /workdir && mkdir -p /tmp && \ @@ -23,8 +34,7 @@ RUN mkdir -p /workdir && mkdir -p /tmp && \ build-essential git curl wget jq pkg-config python3-pip \ libssl-dev libudev-dev -RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb -RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb +# libssl1.1 is not needed for newer versions # Install rust. RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ @@ -32,23 +42,29 @@ RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ rustup component add rustfmt clippy # Install node / npm / yarn. -RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash -ENV NVM_DIR="${HOME}/.nvm" -RUN . $NVM_DIR/nvm.sh && \ - nvm install ${NODE_VERSION} && \ - nvm use ${NODE_VERSION} && \ - nvm alias default node && \ +RUN apt-get install -y nodejs npm && \ npm install -g yarn && \ - yarn add ts-mocha + npm install -g ts-mocha && \ + npm install -g typescript && \ + npm install -g mocha -# Install Solana tools. -RUN sh -c "$(curl -sSfL https://release.solana.com/v${SOLANA_CLI}/install)" +# Install Solana tools (x86_64 version). +RUN curl -sSfL https://github.com/solana-labs/solana/releases/download/v${SOLANA_CLI}/solana-release-x86_64-unknown-linux-gnu.tar.bz2 | tar -xjC /tmp && \ + mv /tmp/solana-release/bin/* /usr/local/bin/ && \ + rm -rf /tmp/solana-release # Install anchor. -RUN cargo install --git https://github.com/coral-xyz/anchor avm --locked --force -RUN avm install ${ANCHOR_CLI} && avm use ${ANCHOR_CLI} +RUN cargo install --git https://github.com/coral-xyz/anchor --tag v${ANCHOR_CLI} anchor-cli --locked + +# Switch to the non-root user for the remaining setup +USER $USERNAME RUN solana-keygen new --no-bip39-passphrase +# Set up Solana config for local development +RUN solana config set --url localhost + +# Create necessary directories +RUN mkdir -p $HOME/.config/solana + WORKDIR /workdir -#be sure to add `/root/.avm/bin` to your PATH to be able to run the installed binaries diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d5aa4e718b..5b4eca3291 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,3 +1,43 @@ { - "build": { "dockerfile": "Dockerfile" }, - } \ No newline at end of file + "name": "Drift Protocol Development", + "build": { + "dockerfile": "Dockerfile", + "platform": "linux/amd64" + }, + "workspaceFolder": "/workdir", + "remoteUser": "vscode", + "mounts": [ + "source=${localWorkspaceFolder},target=/workdir,type=bind,consistency=cached", + "source=drift-target,target=/workdir/target,type=volume,consistency=delegated" + ], + "postCreateCommand": "sudo chown -R vscode:vscode /workdir/target 2>/dev/null || true && echo 'Dev container ready! You can now run: anchor build, anchor test, cargo build, etc.' && echo 'To run tests: bash test-scripts/run-anchor-tests.sh'", + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "ms-vscode.vscode-json", + "tamasfe.even-better-toml" + ], + "settings": { + "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.procMacro.enable": true, + "terminal.integrated.defaultProfile.linux": "bash" + } + } + }, + "forwardPorts": [8899, 8900], + "portsAttributes": { + "8899": { + "label": "Solana Test Validator", + "onAutoForward": "notify" + }, + "8900": { + "label": "Solana Test Validator RPC", + "onAutoForward": "notify" + } + }, + "containerEnv": { + "ANCHOR_WALLET": "/home/vscode/.config/solana/id.json", + "RUST_LOG": "solana_runtime::message_processor::stable_log=debug" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 28e340d853..0bb99c9b0a 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,48 @@ cargo test bash test-scripts/run-anchor-tests.sh ``` +# Development (with devcontainer) + +We've provided a devcontainer `Dockerfile` to help you spin up a dev environment with the correct versions of Rust, Solana, and Anchor for program development. + +Build the container and tag it `drift-dev`: +``` +cd .devcontainer && docker build -t drift-dev . +``` + +Open a shell to the container: +``` +# Find the container ID first +docker ps + +# Then exec into it +docker exec -it /bin/bash +``` + +Alternatively use an extension provided by your IDE to make use of the dev container. For example on vscode/cursor: + +``` +1. Press Ctrl+Shift+P (or Cmd+Shift+P on Mac) +2. Type "Dev Containers: Reopen in Container" +3. Select it and wait for the container to build +4. The IDE terminal should be targeting the dev container now +``` + +Use the dev container as you would a local build environment: +``` +# build program +anchor build + +# update idl +anchor build -- --features anchor-test && cp target/idl/drift.json sdk/src/idl/drift.json + +# run cargo tests +cargo test + +# run typescript tests +bash test-scripts/run-anchor-tests.sh +``` + # Bug Bounty Information about the Bug Bounty can be found [here](./bug-bounty/README.md) From f3a7e66da9e39baa2841a9b9b34479b514c337e1 Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 30 Sep 2025 08:29:37 -0700 Subject: [PATCH 096/159] update idl --- sdk/src/idl/drift.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 51f6e5a084..08848c060c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -16245,6 +16245,13 @@ "option": "publicKey" }, "index": false + }, + { + "name": "signer", + "type": { + "option": "publicKey" + }, + "index": false } ] }, @@ -16828,6 +16835,20 @@ "option": "u64" }, "index": false + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + }, + "index": false + }, + { + "name": "builderFee", + "type": { + "option": "u64" + }, + "index": false } ] }, @@ -19654,4 +19675,4 @@ "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} +} \ No newline at end of file From 61a5657d8bce3554d226e24dc38e3e4817f0053c Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:09:33 -0700 Subject: [PATCH 097/159] bug fixes --- sdk/src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 3fce8c57df..3e9e22d4ac 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1751,6 +1751,7 @@ export type LPPoolAccount = { cumulativeQuoteReceivedFromPerpMarkets: BN; constituents: number; whitelistMint: PublicKey; + tokenSupply: BN; }; export type ConstituentSpotBalance = { From bcb5ded3c0d3e561374b4347f4fbb1996b4d6108 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:09:46 -0700 Subject: [PATCH 098/159] bug fixes --- programs/drift/src/state/lp_pool.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 3bf6896c69..d9ed023897 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -270,11 +270,11 @@ impl LPPool { .price .cast::()? .safe_mul(in_amount_less_fees)?; - let lp_amount = if self.last_aum == 0 { + let lp_amount = if dlp_total_supply == 0 { token_amount_usd.safe_div(token_precision_denominator)? } else { token_amount_usd - .safe_mul(dlp_total_supply.max(1) as u128)? + .safe_mul(dlp_total_supply as u128)? .safe_div(self.last_aum)? .safe_div(token_precision_denominator)? }; From 55b530d435718095681f7a1dcf8f9996d887979c Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 30 Sep 2025 10:14:32 -0700 Subject: [PATCH 099/159] fix node version --- .devcontainer/Dockerfile | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 97523ce216..bae29eae9b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -10,7 +10,7 @@ ARG DEBIAN_FRONTEND=noninteractive ARG SOLANA_CLI="1.16.27" ARG ANCHOR_CLI="0.29.0" -ARG NODE_VERSION="20.18.x" +ARG NODE_VERSION="20.18.1" # Create a non-root user ARG USERNAME=vscode @@ -31,7 +31,7 @@ ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" # Install base utilities. RUN mkdir -p /workdir && mkdir -p /tmp && \ apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \ - build-essential git curl wget jq pkg-config python3-pip \ + build-essential git curl wget jq pkg-config python3-pip xz-utils \ libssl-dev libudev-dev # libssl1.1 is not needed for newer versions @@ -41,12 +41,14 @@ RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ sh rustup.sh -y && \ rustup component add rustfmt clippy -# Install node / npm / yarn. -RUN apt-get install -y nodejs npm && \ - npm install -g yarn && \ - npm install -g ts-mocha && \ - npm install -g typescript && \ - npm install -g mocha +# Install node / npm / yarn (pinned) +RUN curl -fsSLO https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz && \ + tar -xJf node-v${NODE_VERSION}-linux-x64.tar.xz -C /usr/local --strip-components=1 && \ + rm node-v${NODE_VERSION}-linux-x64.tar.xz && \ + corepack enable && \ + npm install -g ts-mocha typescript mocha yarn + +RUN node -v && npm -v # Install Solana tools (x86_64 version). RUN curl -sSfL https://github.com/solana-labs/solana/releases/download/v${SOLANA_CLI}/solana-release-x86_64-unknown-linux-gnu.tar.bz2 | tar -xjC /tmp && \ From dad1a541f899365b961ee3b86eafa8157cece959 Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 30 Sep 2025 11:20:01 -0700 Subject: [PATCH 100/159] update idl --- sdk/src/idl/drift.json | 456 ----------------------------------------- 1 file changed, 456 deletions(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 6bfbba4e7a..08848c060c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,4 @@ { - "version": "2.140.0", "version": "2.140.0", "name": "drift", "instructions": [ @@ -3127,42 +3126,6 @@ ], "args": [] }, - { - "name": "updateDelegateUserGovTokenInsuranceStake", - "accounts": [ - { - "name": "spotMarket", - "isMut": true, - "isSigner": false - }, - { - "name": "insuranceFundStake", - "isMut": false, - "isSigner": false - }, - { - "name": "userStats", - "isMut": true, - "isSigner": false - }, - { - "name": "admin", - "isMut": false, - "isSigner": true - }, - { - "name": "insuranceFundVault", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "initializeInsuranceFundStake", "accounts": [ @@ -3412,77 +3375,53 @@ ] }, { - "name": "beginInsuranceFundSwap", "name": "beginInsuranceFundSwap", "accounts": [ { - "name": "state", "name": "state", "isMut": false, "isSigner": false }, - { - "name": "authority", - "isMut": true, - "isSigner": false - }, { "name": "authority", "isMut": true, "isSigner": true }, { - "name": "outInsuranceFundVault", "name": "outInsuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "inInsuranceFundVault", - "isMut": true, "name": "inInsuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "outTokenAccount", "name": "outTokenAccount", "isMut": true, "isSigner": false }, { - "name": "inTokenAccount", "name": "inTokenAccount", "isMut": true, "isSigner": false }, { - "name": "ifRebalanceConfig", "name": "ifRebalanceConfig", "isMut": true, "isSigner": false }, { - "name": "tokenProgram", "name": "tokenProgram", "isMut": false, "isSigner": false - "isSigner": false }, { - "name": "driftSigner", "name": "driftSigner", "isMut": false, "isSigner": false }, - { - "name": "instructions", - "isMut": false, - "isSigner": false, - "docs": [ - "Instructions Sysvar for instruction introspection" - ] - }, { "name": "instructions", "isMut": false, @@ -3494,7 +3433,6 @@ ], "args": [ { - "name": "inMarketIndex", "name": "inMarketIndex", "type": "u16" }, @@ -3502,12 +3440,6 @@ "name": "outMarketIndex", "type": "u16" }, - { - "name": "amountIn", - "type": "u64" - "name": "outMarketIndex", - "type": "u16" - }, { "name": "amountIn", "type": "u64" @@ -3515,7 +3447,6 @@ ] }, { - "name": "endInsuranceFundSwap", "name": "endInsuranceFundSwap", "accounts": [ { @@ -3584,7 +3515,6 @@ ] }, { - "name": "transferProtocolIfSharesToRevenuePool", "name": "transferProtocolIfSharesToRevenuePool", "accounts": [ { @@ -3598,13 +3528,11 @@ "isSigner": true }, { - "name": "insuranceFundVault", "name": "insuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "spotMarketVault", "name": "spotMarketVault", "isMut": true, "isSigner": false @@ -3627,56 +3555,38 @@ ], "args": [ { - "name": "marketIndex", "name": "marketIndex", "type": "u16" }, { "name": "amount", "type": "u64" - "name": "amount", - "type": "u64" } ] }, { - "name": "depositIntoInsuranceFundStake", "name": "depositIntoInsuranceFundStake", "accounts": [ { - "name": "signer", "name": "signer", "isMut": false, "isSigner": true }, - { - "name": "state", - "isMut": true, - "isSigner": true - }, { "name": "state", "isMut": true, "isSigner": false }, { - "name": "spotMarket", "name": "spotMarket", "isMut": true, "isSigner": false - "isSigner": false }, { "name": "insuranceFundStake", "isMut": true, "isSigner": false }, - { - "name": "userStats", - "name": "insuranceFundStake", - "isMut": true, - "isSigner": false - }, { "name": "userStats", "isMut": true, @@ -3692,12 +3602,6 @@ "isMut": true, "isSigner": false }, - { - "name": "userTokenAccount", - "name": "insuranceFundVault", - "isMut": true, - "isSigner": false - }, { "name": "userTokenAccount", "isMut": true, @@ -10765,106 +10669,6 @@ ] } }, - { - "name": "RevenueShare", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "docs": [ - "the owner of this account, a builder or referrer" - ], - "type": "publicKey" - }, - { - "name": "totalReferrerRewards", - "type": "u64" - }, - { - "name": "totalBuilderRewards", - "type": "u64" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 18 - ] - } - } - ] - } - }, - { - "name": "RevenueShareEscrow", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "docs": [ - "the owner of this account, a user" - ], - "type": "publicKey" - }, - { - "name": "referrer", - "type": "publicKey" - }, - { - "name": "referrerBoostExpireTs", - "type": "u32" - }, - { - "name": "referrerRewardOffset", - "type": "i8" - }, - { - "name": "refereeFeeNumeratorOffset", - "type": "i8" - }, - { - "name": "referrerBoostNumerator", - "type": "i8" - }, - { - "name": "reservedFixed", - "type": { - "array": [ - "u8", - 17 - ] - } - }, - { - "name": "padding0", - "type": "u32" - }, - { - "name": "orders", - "type": { - "vec": { - "defined": "RevenueShareOrder" - } - } - }, - { - "name": "padding1", - "type": "u32" - }, - { - "name": "approvedBuilders", - "type": { - "vec": { - "defined": "BuilderInfo" - } - } - } - ] - } - }, { "name": "SignedMsgUserOrders", "docs": [ @@ -13171,18 +12975,6 @@ "option": "u8" } }, - { - "name": "builderFeeTenthBps", - "type": { - "option": "u16" - } - }, - { - "name": "builderIdx", - "type": { - "option": "u8" - } - }, { "name": "builderFeeTenthBps", "type": { @@ -13248,18 +13040,6 @@ "option": "u8" } }, - { - "name": "builderFeeTenthBps", - "type": { - "option": "u16" - } - }, - { - "name": "builderIdx", - "type": { - "option": "u8" - } - }, { "name": "builderFeeTenthBps", "type": { @@ -14268,157 +14048,6 @@ ] } }, - { - "name": "RevenueShareOrder", - "type": { - "kind": "struct", - "fields": [ - { - "name": "feesAccrued", - "docs": [ - "fees accrued so far for this order slot. This is not exclusively fees from this order_id", - "and may include fees from other orders in the same market. This may be swept to the", - "builder's SpotPosition during settle_pnl." - ], - "type": "u64" - }, - { - "name": "orderId", - "docs": [ - "the order_id of the current active order in this slot. It's only relevant while bit_flag = Open" - ], - "type": "u32" - }, - { - "name": "feeTenthBps", - "docs": [ - "the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01%" - ], - "type": "u16" - }, - { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "subAccountId", - "docs": [ - "the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open" - ], - "type": "u16" - }, - { - "name": "builderIdx", - "docs": [ - "the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored", - "if bit_flag = Referral." - ], - "type": "u8" - }, - { - "name": "bitFlags", - "docs": [ - "bitflags that describe the state of the order.", - "[`RevenueShareOrderBitFlag::Init`]: this order slot is available for use.", - "[`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order.", - "[`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into.", - "the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders.", - "[`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market.", - "If it is set, no other bitflag should be set." - ], - "type": "u8" - }, - { - "name": "userOrderIndex", - "docs": [ - "the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches." - ], - "type": "u8" - }, - { - "name": "marketType", - "type": { - "defined": "MarketType" - } - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 10 - ] - } - } - ] - } - }, - { - "name": "BuilderInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "publicKey" - }, - { - "name": "maxFeeTenthBps", - "type": "u16" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 6 - ] - } - } - ] - } - }, - { - "name": "RevenueShareEscrowFixed", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "publicKey" - }, - { - "name": "referrer", - "type": "publicKey" - }, - { - "name": "referrerBoostExpireTs", - "type": "u32" - }, - { - "name": "referrerRewardOffset", - "type": "i8" - }, - { - "name": "refereeFeeNumeratorOffset", - "type": "i8" - }, - { - "name": "referrerBoostNumerator", - "type": "i8" - }, - { - "name": "reservedFixed", - "type": { - "array": [ - "u8", - 17 - ] - } - } - ] - } - }, { "name": "SignedMsgOrderId", "type": { @@ -15623,9 +15252,6 @@ { "name": "StakeTransfer" }, - { - "name": "AdminDeposit" - }, { "name": "AdminDeposit" } @@ -16115,26 +15741,6 @@ ] } }, - { - "name": "RevenueShareOrderBitFlag", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Init" - }, - { - "name": "Open" - }, - { - "name": "Completed" - }, - { - "name": "Referral" - } - ] - } - }, { "name": "SettlePnlMode", "type": { @@ -16414,9 +16020,6 @@ { "name": "NewTriggerReduceOnly" }, - { - "name": "HasBuilder" - }, { "name": "HasBuilder" } @@ -16434,9 +16037,6 @@ { "name": "IsReferred" }, - { - "name": "BuilderReferral" - }, { "name": "BuilderReferral" } @@ -18022,62 +17622,6 @@ } ] }, - { - "name": "RevenueShareSettleRecord", - "fields": [ - { - "name": "ts", - "type": "i64", - "index": false - }, - { - "name": "builder", - "type": { - "option": "publicKey" - }, - "index": false - }, - { - "name": "referrer", - "type": { - "option": "publicKey" - }, - "index": false - }, - { - "name": "feeSettled", - "type": "u64", - "index": false - }, - { - "name": "marketIndex", - "type": "u16", - "index": false - }, - { - "name": "marketType", - "type": { - "defined": "MarketType" - }, - "index": false - }, - { - "name": "builderSubAccountId", - "type": "u16", - "index": false - }, - { - "name": "builderTotalReferrerRewards", - "type": "u64", - "index": false - }, - { - "name": "builderTotalBuilderRewards", - "type": "u64", - "index": false - } - ] - }, { "name": "LPSettleRecord", "fields": [ From dfec9a32df865a92edb721724879d04af90ad57a Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:31:38 -0700 Subject: [PATCH 101/159] fixed idl --- sdk/src/idl/drift.json | 479 ++--------------------------------------- 1 file changed, 22 insertions(+), 457 deletions(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 60d209fd95..08848c060c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,4 @@ { - "version": "2.140.0", "version": "2.140.0", "name": "drift", "instructions": [ @@ -3127,42 +3126,6 @@ ], "args": [] }, - { - "name": "updateDelegateUserGovTokenInsuranceStake", - "accounts": [ - { - "name": "spotMarket", - "isMut": true, - "isSigner": false - }, - { - "name": "insuranceFundStake", - "isMut": false, - "isSigner": false - }, - { - "name": "userStats", - "isMut": true, - "isSigner": false - }, - { - "name": "admin", - "isMut": false, - "isSigner": true - }, - { - "name": "insuranceFundVault", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "initializeInsuranceFundStake", "accounts": [ @@ -3412,77 +3375,53 @@ ] }, { - "name": "beginInsuranceFundSwap", "name": "beginInsuranceFundSwap", "accounts": [ { - "name": "state", "name": "state", "isMut": false, "isSigner": false }, - { - "name": "authority", - "isMut": true, - "isSigner": false - }, { "name": "authority", "isMut": true, "isSigner": true }, { - "name": "outInsuranceFundVault", "name": "outInsuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "inInsuranceFundVault", - "isMut": true, "name": "inInsuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "outTokenAccount", "name": "outTokenAccount", "isMut": true, "isSigner": false }, { - "name": "inTokenAccount", "name": "inTokenAccount", "isMut": true, "isSigner": false }, { - "name": "ifRebalanceConfig", "name": "ifRebalanceConfig", "isMut": true, "isSigner": false }, { - "name": "tokenProgram", "name": "tokenProgram", "isMut": false, "isSigner": false - "isSigner": false }, { - "name": "driftSigner", "name": "driftSigner", "isMut": false, "isSigner": false }, - { - "name": "instructions", - "isMut": false, - "isSigner": false, - "docs": [ - "Instructions Sysvar for instruction introspection" - ] - }, { "name": "instructions", "isMut": false, @@ -3495,16 +3434,9 @@ "args": [ { "name": "inMarketIndex", - "name": "inMarketIndex", - "type": "u16" - }, - { - "name": "outMarketIndex", "type": "u16" }, { - "name": "amountIn", - "type": "u64" "name": "outMarketIndex", "type": "u16" }, @@ -3515,7 +3447,6 @@ ] }, { - "name": "endInsuranceFundSwap", "name": "endInsuranceFundSwap", "accounts": [ { @@ -3584,7 +3515,6 @@ ] }, { - "name": "transferProtocolIfSharesToRevenuePool", "name": "transferProtocolIfSharesToRevenuePool", "accounts": [ { @@ -3598,13 +3528,11 @@ "isSigner": true }, { - "name": "insuranceFundVault", "name": "insuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "spotMarketVault", "name": "spotMarketVault", "isMut": true, "isSigner": false @@ -3627,56 +3555,38 @@ ], "args": [ { - "name": "marketIndex", "name": "marketIndex", "type": "u16" }, { "name": "amount", "type": "u64" - "name": "amount", - "type": "u64" } ] }, { - "name": "depositIntoInsuranceFundStake", "name": "depositIntoInsuranceFundStake", "accounts": [ { - "name": "signer", "name": "signer", "isMut": false, "isSigner": true }, - { - "name": "state", - "isMut": true, - "isSigner": true - }, { "name": "state", "isMut": true, "isSigner": false }, { - "name": "spotMarket", "name": "spotMarket", "isMut": true, "isSigner": false - "isSigner": false }, { "name": "insuranceFundStake", "isMut": true, "isSigner": false }, - { - "name": "userStats", - "name": "insuranceFundStake", - "isMut": true, - "isSigner": false - }, { "name": "userStats", "isMut": true, @@ -3692,12 +3602,6 @@ "isMut": true, "isSigner": false }, - { - "name": "userTokenAccount", - "name": "insuranceFundVault", - "isMut": true, - "isSigner": false - }, { "name": "userTokenAccount", "isMut": true, @@ -10765,106 +10669,6 @@ ] } }, - { - "name": "RevenueShare", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "docs": [ - "the owner of this account, a builder or referrer" - ], - "type": "publicKey" - }, - { - "name": "totalReferrerRewards", - "type": "u64" - }, - { - "name": "totalBuilderRewards", - "type": "u64" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 18 - ] - } - } - ] - } - }, - { - "name": "RevenueShareEscrow", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "docs": [ - "the owner of this account, a user" - ], - "type": "publicKey" - }, - { - "name": "referrer", - "type": "publicKey" - }, - { - "name": "referrerBoostExpireTs", - "type": "u32" - }, - { - "name": "referrerRewardOffset", - "type": "i8" - }, - { - "name": "refereeFeeNumeratorOffset", - "type": "i8" - }, - { - "name": "referrerBoostNumerator", - "type": "i8" - }, - { - "name": "reservedFixed", - "type": { - "array": [ - "u8", - 17 - ] - } - }, - { - "name": "padding0", - "type": "u32" - }, - { - "name": "orders", - "type": { - "vec": { - "defined": "RevenueShareOrder" - } - } - }, - { - "name": "padding1", - "type": "u32" - }, - { - "name": "approvedBuilders", - "type": { - "vec": { - "defined": "BuilderInfo" - } - } - } - ] - } - }, { "name": "SignedMsgUserOrders", "docs": [ @@ -13171,18 +12975,6 @@ "option": "u8" } }, - { - "name": "builderFeeTenthBps", - "type": { - "option": "u16" - } - }, - { - "name": "builderIdx", - "type": { - "option": "u8" - } - }, { "name": "builderFeeTenthBps", "type": { @@ -13248,18 +13040,6 @@ "option": "u8" } }, - { - "name": "builderFeeTenthBps", - "type": { - "option": "u16" - } - }, - { - "name": "builderIdx", - "type": { - "option": "u8" - } - }, { "name": "builderFeeTenthBps", "type": { @@ -14268,157 +14048,6 @@ ] } }, - { - "name": "RevenueShareOrder", - "type": { - "kind": "struct", - "fields": [ - { - "name": "feesAccrued", - "docs": [ - "fees accrued so far for this order slot. This is not exclusively fees from this order_id", - "and may include fees from other orders in the same market. This may be swept to the", - "builder's SpotPosition during settle_pnl." - ], - "type": "u64" - }, - { - "name": "orderId", - "docs": [ - "the order_id of the current active order in this slot. It's only relevant while bit_flag = Open" - ], - "type": "u32" - }, - { - "name": "feeTenthBps", - "docs": [ - "the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01%" - ], - "type": "u16" - }, - { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "subAccountId", - "docs": [ - "the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open" - ], - "type": "u16" - }, - { - "name": "builderIdx", - "docs": [ - "the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored", - "if bit_flag = Referral." - ], - "type": "u8" - }, - { - "name": "bitFlags", - "docs": [ - "bitflags that describe the state of the order.", - "[`RevenueShareOrderBitFlag::Init`]: this order slot is available for use.", - "[`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order.", - "[`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into.", - "the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders.", - "[`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market.", - "If it is set, no other bitflag should be set." - ], - "type": "u8" - }, - { - "name": "userOrderIndex", - "docs": [ - "the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches." - ], - "type": "u8" - }, - { - "name": "marketType", - "type": { - "defined": "MarketType" - } - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 10 - ] - } - } - ] - } - }, - { - "name": "BuilderInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "publicKey" - }, - { - "name": "maxFeeTenthBps", - "type": "u16" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 6 - ] - } - } - ] - } - }, - { - "name": "RevenueShareEscrowFixed", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "publicKey" - }, - { - "name": "referrer", - "type": "publicKey" - }, - { - "name": "referrerBoostExpireTs", - "type": "u32" - }, - { - "name": "referrerRewardOffset", - "type": "i8" - }, - { - "name": "refereeFeeNumeratorOffset", - "type": "i8" - }, - { - "name": "referrerBoostNumerator", - "type": "i8" - }, - { - "name": "reservedFixed", - "type": { - "array": [ - "u8", - 17 - ] - } - } - ] - } - }, { "name": "SignedMsgOrderId", "type": { @@ -15623,9 +15252,6 @@ { "name": "StakeTransfer" }, - { - "name": "AdminDeposit" - }, { "name": "AdminDeposit" } @@ -16115,26 +15741,6 @@ ] } }, - { - "name": "RevenueShareOrderBitFlag", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Init" - }, - { - "name": "Open" - }, - { - "name": "Completed" - }, - { - "name": "Referral" - } - ] - } - }, { "name": "SettlePnlMode", "type": { @@ -16414,9 +16020,6 @@ { "name": "NewTriggerReduceOnly" }, - { - "name": "HasBuilder" - }, { "name": "HasBuilder" } @@ -16434,9 +16037,6 @@ { "name": "IsReferred" }, - { - "name": "BuilderReferral" - }, { "name": "BuilderReferral" } @@ -16645,6 +16245,13 @@ "option": "publicKey" }, "index": false + }, + { + "name": "signer", + "type": { + "option": "publicKey" + }, + "index": false } ] }, @@ -17228,6 +16835,20 @@ "option": "u64" }, "index": false + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + }, + "index": false + }, + { + "name": "builderFee", + "type": { + "option": "u64" + }, + "index": false } ] }, @@ -18001,62 +17622,6 @@ } ] }, - { - "name": "RevenueShareSettleRecord", - "fields": [ - { - "name": "ts", - "type": "i64", - "index": false - }, - { - "name": "builder", - "type": { - "option": "publicKey" - }, - "index": false - }, - { - "name": "referrer", - "type": { - "option": "publicKey" - }, - "index": false - }, - { - "name": "feeSettled", - "type": "u64", - "index": false - }, - { - "name": "marketIndex", - "type": "u16", - "index": false - }, - { - "name": "marketType", - "type": { - "defined": "MarketType" - }, - "index": false - }, - { - "name": "builderSubAccountId", - "type": "u16", - "index": false - }, - { - "name": "builderTotalReferrerRewards", - "type": "u64", - "index": false - }, - { - "name": "builderTotalBuilderRewards", - "type": "u64", - "index": false - } - ] - }, { "name": "LPSettleRecord", "fields": [ @@ -20110,4 +19675,4 @@ "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} +} \ No newline at end of file From 199f5047b2074c4e05f68a79f90c4139d5a254ac Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:23:11 +0000 Subject: [PATCH 102/159] dockerfile and dev container working, and anchor build working --- .devcontainer/Dockerfile | 95 +++++++++++++-------------------- .devcontainer/devcontainer.json | 20 +++---- sdk/src/idl/drift.json | 7 --- 3 files changed, 43 insertions(+), 79 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index bb4aa491ba..2f9b3dcf3e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,71 +1,48 @@ # -# Docker image to generate deterministic, verifiable builds of Anchor programs. +# Drift Protocol Dev Container # FROM --platform=linux/amd64 rust:1.70.0 ARG DEBIAN_FRONTEND=noninteractive - ARG SOLANA_CLI="1.16.27" ARG ANCHOR_CLI="0.29.0" ARG NODE_VERSION="20.18.1" -# Create a non-root user -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ - && apt-get update \ - && apt-get install -y sudo \ - && echo "$USERNAME ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME - -ENV HOME="/home/$USERNAME" -ENV PATH="${HOME}/.cargo/bin:${PATH}" -ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" - -# Install base utilities -RUN mkdir -p /workdir && mkdir -p /tmp && \ - apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \ - build-essential git curl wget jq pkg-config python3-pip xz-utils \ - libssl-dev libudev-dev - -# Install Rust (latest via rustup, ensures toolchain components are present) -RUN curl -fsSL https://sh.rustup.rs -o /tmp/rustup.sh && \ - sh /tmp/rustup.sh -y && \ - rustup component add rustfmt clippy && \ - rm /tmp/rustup.sh - -# Install Node.js (pinned) -RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz -o /tmp/node.tar.xz \ - && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \ - && rm /tmp/node.tar.xz \ - && corepack enable \ - && npm install -g ts-mocha typescript mocha - -RUN node -v && npm -v && yarn -v - -# Install Solana tools (x86_64 version) -RUN curl -sSfL https://github.com/solana-labs/solana/releases/download/v${SOLANA_CLI}/solana-release-x86_64-unknown-linux-gnu.tar.bz2 \ - | tar -xjC /tmp && \ - mv /tmp/solana-release/bin/* /usr/local/bin/ && \ - rm -rf /tmp/solana-release - -# Install Anchor -RUN cargo install --git https://github.com/coral-xyz/anchor --tag v${ANCHOR_CLI} anchor-cli --locked - -# Switch to the non-root user for the remaining setup -USER $USERNAME - -# Generate a default Solana keypair -RUN solana-keygen new --no-bip39-passphrase - -# Set up Solana config for local development -RUN solana config set --url localhost - -# Create necessary directories -RUN mkdir -p $HOME/.config/solana +ENV HOME="/root" +ENV PATH="/usr/local/cargo/bin:${PATH}" +ENV PATH="/root/.local/share/solana/install/active_release/bin:${PATH}" + +RUN mkdir -p /workdir /tmp && \ + apt-get update -qq && apt-get upgrade -qq && apt-get install -y --no-install-recommends \ + build-essential git curl wget jq pkg-config python3-pip xz-utils ca-certificates \ + libssl-dev libudev-dev bash && \ + rm -rf /var/lib/apt/lists/* + +RUN rustup component add rustfmt clippy + +RUN curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz \ + && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \ + && rm /tmp/node.tar.xz \ + && corepack enable \ + && npm install -g ts-mocha typescript mocha \ + && node -v && npm -v && yarn -v + +# Solana CLI (x86_64 build) +RUN curl -sSfL "https://github.com/solana-labs/solana/releases/download/v${SOLANA_CLI}/solana-release-x86_64-unknown-linux-gnu.tar.bz2" \ + | tar -xjC /tmp \ + && mv /tmp/solana-release/bin/* /usr/local/bin/ \ + && rm -rf /tmp/solana-release + +# Anchor CLI +RUN cargo install --git https://github.com/coral-xyz/anchor --tag "v${ANCHOR_CLI}" anchor-cli --locked + +# Set up Solana key + config for root +RUN solana-keygen new --no-bip39-passphrase --force \ + && solana config set --url localhost + +RUN apt-get update && apt-get install -y zsh curl git \ + && sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended \ + && chsh -s /usr/bin/zsh vscode WORKDIR /workdir diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5b4eca3291..d91b677b22 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,16 +1,16 @@ { - "name": "Drift Protocol Development", + "name": "Drift Protocol Development (amd64, root)", "build": { "dockerfile": "Dockerfile", "platform": "linux/amd64" }, "workspaceFolder": "/workdir", - "remoteUser": "vscode", + "remoteUser": "root", "mounts": [ "source=${localWorkspaceFolder},target=/workdir,type=bind,consistency=cached", "source=drift-target,target=/workdir/target,type=volume,consistency=delegated" ], - "postCreateCommand": "sudo chown -R vscode:vscode /workdir/target 2>/dev/null || true && echo 'Dev container ready! You can now run: anchor build, anchor test, cargo build, etc.' && echo 'To run tests: bash test-scripts/run-anchor-tests.sh'", + "postCreateCommand": "echo 'Dev container ready. Run: anchor build / anchor test / cargo build'", "customizations": { "vscode": { "extensions": [ @@ -27,17 +27,11 @@ }, "forwardPorts": [8899, 8900], "portsAttributes": { - "8899": { - "label": "Solana Test Validator", - "onAutoForward": "notify" - }, - "8900": { - "label": "Solana Test Validator RPC", - "onAutoForward": "notify" - } + "8899": { "label": "Solana Test Validator", "onAutoForward": "notify" }, + "8900": { "label": "Solana Test Validator RPC", "onAutoForward": "notify" } }, "containerEnv": { - "ANCHOR_WALLET": "/home/vscode/.config/solana/id.json", + "ANCHOR_WALLET": "/root/.config/solana/id.json", "RUST_LOG": "solana_runtime::message_processor::stable_log=debug" } -} \ No newline at end of file +} diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 08848c060c..0eee259348 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -16245,13 +16245,6 @@ "option": "publicKey" }, "index": false - }, - { - "name": "signer", - "type": { - "option": "publicKey" - }, - "index": false } ] }, From 19efd623ffa8a3c02f80a087f6bdaf6be11f367d Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:56:19 +0000 Subject: [PATCH 103/159] add dev container.json and dockerfile --- .devcontainer/Dockerfile | 5 +++-- .devcontainer/devcontainer.json | 31 ++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2f9b3dcf3e..39d1ca340f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -19,7 +19,8 @@ RUN mkdir -p /workdir /tmp && \ libssl-dev libudev-dev bash && \ rm -rf /var/lib/apt/lists/* -RUN rustup component add rustfmt clippy +RUN rustup install 1.78.0 \ + && rustup component add rustfmt clippy --toolchain 1.78.0 RUN curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz \ && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \ @@ -43,6 +44,6 @@ RUN solana-keygen new --no-bip39-passphrase --force \ RUN apt-get update && apt-get install -y zsh curl git \ && sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended \ - && chsh -s /usr/bin/zsh vscode + && chsh -s /usr/bin/zsh root WORKDIR /workdir diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d91b677b22..3228a43367 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "name": "Drift Protocol Development (amd64, root)", + "name": "Drift Protocol Development", "build": { "dockerfile": "Dockerfile", "platform": "linux/amd64" @@ -10,7 +10,7 @@ "source=${localWorkspaceFolder},target=/workdir,type=bind,consistency=cached", "source=drift-target,target=/workdir/target,type=volume,consistency=delegated" ], - "postCreateCommand": "echo 'Dev container ready. Run: anchor build / anchor test / cargo build'", + "postCreateCommand": "yarn config set ignore-package-manager true && echo 'Dev container ready! You can now run: anchor build, anchor test, cargo build, etc.'", "customizations": { "vscode": { "extensions": [ @@ -19,19 +19,36 @@ "tamasfe.even-better-toml" ], "settings": { + "rust-analyzer.cachePriming.numThreads": 1, "rust-analyzer.cargo.buildScripts.enable": true, "rust-analyzer.procMacro.enable": true, - "terminal.integrated.defaultProfile.linux": "bash" + "rust-analyzer.checkOnSave": true, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.server.extraEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + "RUSTUP_TOOLCHAIN": "1.78.0-x86_64-unknown-linux-gnu" + }, + "editor.formatOnSave": true, + "git.ignoreLimitWarning": true } } }, - "forwardPorts": [8899, 8900], + "forwardPorts": [ + 8899, + 8900 + ], "portsAttributes": { - "8899": { "label": "Solana Test Validator", "onAutoForward": "notify" }, - "8900": { "label": "Solana Test Validator RPC", "onAutoForward": "notify" } + "8899": { + "label": "Solana Test Validator", + "onAutoForward": "notify" + }, + "8900": { + "label": "Solana Test Validator RPC", + "onAutoForward": "notify" + } }, "containerEnv": { "ANCHOR_WALLET": "/root/.config/solana/id.json", "RUST_LOG": "solana_runtime::message_processor::stable_log=debug" } -} +} \ No newline at end of file From 69130152a2313dfd9c40e47ba93b7b818474dcfd Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 1 Oct 2025 09:32:01 +0800 Subject: [PATCH 104/159] fix bug --- programs/drift/src/instructions/user.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index db9578d669..d3ec7cc666 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -653,7 +653,7 @@ pub fn handle_deposit<'c: 'info, 'info>( } else { DepositExplanation::None }; - let signer = if ctx.accounts.authority.key() == user.authority { + let signer = if ctx.accounts.authority.key() != user.authority { Some(ctx.accounts.authority.key()) } else { None From 2df49ea5726d9edefbacf37d88dc97f3cde927db Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 1 Oct 2025 09:32:49 +0800 Subject: [PATCH 105/159] cargo fmt -- --- programs/drift/src/instructions/user.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index d3ec7cc666..ac60cb0d26 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -4152,9 +4152,7 @@ pub struct InitializeReferrerName<'info> { #[instruction(market_index: u16,)] pub struct Deposit<'info> { pub state: Box>, - #[account( - mut - )] + #[account(mut)] pub user: AccountLoader<'info, User>, #[account( mut, From c768376a69bdb7885d5614e3128b96224d8ab372 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:59:14 +0000 Subject: [PATCH 106/159] update dlp types to include lp pool key --- .devcontainer/Dockerfile | 6 ++++++ package.json | 5 +++-- programs/drift/src/instructions/keeper.rs | 2 ++ programs/drift/src/instructions/lp_pool.rs | 6 ++++++ programs/drift/src/state/events.rs | 13 ++++++++++--- sdk/src/idl/drift.json | 20 ++++++++++++++++++++ sdk/src/types.ts | 10 +++++++--- 7 files changed, 54 insertions(+), 8 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 39d1ca340f..d9735fa13c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -19,6 +19,12 @@ RUN mkdir -p /workdir /tmp && \ libssl-dev libudev-dev bash && \ rm -rf /var/lib/apt/lists/* +RUN apt-get install -y software-properties-common \ + && add-apt-repository 'deb http://deb.debian.org/debian bookworm main' \ + && apt-get update && apt-get install -y libc6 libc6-dev + +RUN rustup component add rustfmt + RUN rustup install 1.78.0 \ && rustup component add rustfmt clippy --toolchain 1.78.0 diff --git a/package.json b/package.json index 854fea3020..51bd328a8d 100644 --- a/package.json +++ b/package.json @@ -94,5 +94,6 @@ "chalk-template": "<1.1.1", "supports-hyperlinks": "<4.1.1", "has-ansi": "<6.0.1" - } -} \ No newline at end of file + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 2b8fc96af6..88c86033a2 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -3333,6 +3333,7 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( ctx.accounts.amm_cache.load_zc_mut()?; let quote_market = &mut ctx.accounts.quote_market.load_mut()?; let mut quote_constituent = ctx.accounts.constituent.load_mut()?; + let lp_pool_key = ctx.accounts.lp_pool.key(); let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); @@ -3486,6 +3487,7 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( .cast::()?, lp_aum: lp_pool.last_aum, lp_price: lp_pool.get_price(lp_pool.token_supply)?, + lp_pool: lp_pool_key, }); // Calculate new quote owed amount diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index b4cd8b1c0d..bd718eb1d4 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -483,6 +483,7 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( out_market_target_weight: out_target_weight, in_swap_id, out_swap_id, + lp_pool: lp_pool_key, })?; receive( @@ -848,6 +849,7 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( lp_pool.last_aum, )?, in_market_target_weight: in_target_weight, + lp_pool: lp_pool_key, })?; Ok(()) @@ -1230,6 +1232,7 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( lp_pool.last_aum, )?, in_market_target_weight: out_target_weight, + lp_pool: lp_pool_key, })?; Ok(()) @@ -1371,6 +1374,7 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); let mut constituent = ctx.accounts.constituent.load_mut()?; + let lp_pool_key = constituent.lp_pool; if amount == 0 { return Err(ErrorCode::InsufficientDeposit.into()); @@ -1478,6 +1482,7 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( last_token_balance: constituent.last_spot_balance_token_amount, interest_accrued_token_amount, amount_deposit_withdraw: amount, + lp_pool: lp_pool_key, }); constituent.last_spot_balance_token_amount = new_token_balance; constituent.cumulative_spot_interest_accrued_token_amount = constituent @@ -1561,6 +1566,7 @@ pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( last_token_balance: constituent.last_spot_balance_token_amount, interest_accrued_token_amount, amount_deposit_withdraw: amount, + lp_pool: constituent.lp_pool, }); constituent.last_spot_balance_token_amount = new_token_balance; constituent.cumulative_spot_interest_accrued_token_amount = constituent diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 63811d8b94..cdf97002e7 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -789,6 +789,8 @@ pub struct LPSettleRecord { pub lp_aum: u128, // current mint price of lp pub lp_price: u128, + // lp pool pubkey + pub lp_pool: Pubkey, } #[event] @@ -830,10 +832,12 @@ pub struct LPSwapRecord { pub out_market_target_weight: i64, pub in_swap_id: u64, pub out_swap_id: u64, + // lp pool pubkey + pub lp_pool: Pubkey, } impl Size for LPSwapRecord { - const SIZE: usize = 376; + const SIZE: usize = 408; } #[event] @@ -868,10 +872,12 @@ pub struct LPMintRedeemRecord { /// PERCENTAGE_PRECISION pub in_market_current_weight: i64, pub in_market_target_weight: i64, + // lp pool pubkey + pub lp_pool: Pubkey, } impl Size for LPMintRedeemRecord { - const SIZE: usize = 328; + const SIZE: usize = 360; } #[event] @@ -886,8 +892,9 @@ pub struct LPBorrowLendDepositRecord { pub last_token_balance: i64, pub interest_accrued_token_amount: i64, pub amount_deposit_withdraw: u64, + pub lp_pool: Pubkey, } impl Size for LPBorrowLendDepositRecord { - const SIZE: usize = 72; + const SIZE: usize = 104; } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 0eee259348..426ec2662a 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -17672,6 +17672,11 @@ "name": "lpPrice", "type": "u128", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] }, @@ -17782,6 +17787,11 @@ "name": "outSwapId", "type": "u64", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] }, @@ -17877,6 +17887,11 @@ "name": "inMarketTargetWeight", "type": "i64", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] }, @@ -17929,6 +17944,11 @@ "name": "amountDepositWithdraw", "type": "u64", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 3e9e22d4ac..ec3fe0ed47 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -767,6 +767,7 @@ export type LPSwapRecord = { outMarketTargetWeight: BN; inSwapId: BN; outSwapId: BN; + lpPool: PublicKey; }; export type LPMintRedeemRecord = { @@ -789,6 +790,7 @@ export type LPMintRedeemRecord = { lastAumSlot: BN; inMarketCurrentWeight: BN; inMarketTargetWeight: BN; + lpPool: PublicKey; }; export type LPSettleRecord = { @@ -803,6 +805,7 @@ export type LPSettleRecord = { perpAmmExFeeDelta: BN; lpAum: BN; lpPrice: BN; + lpPool: PublicKey; }; export type LPBorrowLendDepositRecord = { @@ -815,6 +818,7 @@ export type LPBorrowLendDepositRecord = { lastTokenBalance: BN; interestAccruedTokenAmount: BN; amountDepositWithdraw: BN; + lpPool: PublicKey; }; export type StateAccount = { @@ -1411,9 +1415,9 @@ export interface IWallet { publicKey: PublicKey; payer?: Keypair; supportedTransactionVersions?: - | ReadonlySet - | null - | undefined; + | ReadonlySet + | null + | undefined; } export interface IVersionedWallet { signVersionedTransaction( From 340b6c9cc55760e2ca09b07896a36f7daa0a6525 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:29:31 -0700 Subject: [PATCH 107/159] stable target base liquidity fix + cleanup --- .devcontainer/Dockerfile | 17 +++++----- programs/drift/src/instructions/lp_pool.rs | 11 ++----- programs/drift/src/math/lp_pool.rs | 7 ++-- programs/drift/src/state/lp_pool.rs | 37 +++++++++++----------- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d9735fa13c..7b60e59064 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -13,15 +13,16 @@ ENV HOME="/root" ENV PATH="/usr/local/cargo/bin:${PATH}" ENV PATH="/root/.local/share/solana/install/active_release/bin:${PATH}" -RUN mkdir -p /workdir /tmp && \ - apt-get update -qq && apt-get upgrade -qq && apt-get install -y --no-install-recommends \ - build-essential git curl wget jq pkg-config python3-pip xz-utils ca-certificates \ - libssl-dev libudev-dev bash && \ - rm -rf /var/lib/apt/lists/* - -RUN apt-get install -y software-properties-common \ +RUN mkdir -p /workdir /tmp \ + && apt-get update -qq \ + && apt-get upgrade -qq \ + && apt-get install -y --no-install-recommends \ + build-essential git curl wget jq pkg-config python3-pip xz-utils ca-certificates \ + libssl-dev libudev-dev bash software-properties-common \ && add-apt-repository 'deb http://deb.debian.org/debian bookworm main' \ - && apt-get update && apt-get install -y libc6 libc6-dev + && apt-get update -qq \ + && apt-get install -y libc6 libc6-dev \ + && rm -rf /var/lib/apt/lists/* RUN rustup component add rustfmt diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index bd718eb1d4..3f7745348c 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -223,10 +223,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( // Set USDC stable weight msg!("aum: {}", aum); - let total_stable_target_base = aum - .cast::()? - .safe_sub(crypto_delta.abs())? - .max(0_i128); + let total_stable_target_base = aum.cast::()?.safe_sub(crypto_delta)?; constituent_target_base .get_mut(lp_pool.quote_consituent_index as u32) .target_base = total_stable_target_base.cast::()?; @@ -861,7 +858,6 @@ pub fn handle_view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( in_amount: u128, ) -> Result<()> { let slot = Clock::get()?.slot; - let now = Clock::get()?.unix_timestamp; let state = &ctx.accounts.state; let lp_pool = ctx.accounts.lp_pool.load()?; @@ -1043,7 +1039,7 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, )?; - let out_oracle = out_oracle.clone(); + let out_oracle = *out_oracle; // TODO: check self.aum validity @@ -1247,7 +1243,6 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( lp_to_burn: u64, ) -> Result<()> { let slot = Clock::get()?.slot; - let now = Clock::get()?.unix_timestamp; let state = &ctx.accounts.state; let lp_pool = ctx.accounts.lp_pool.load()?; @@ -1520,7 +1515,7 @@ pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( controller::spot_balance::update_spot_market_cumulative_interest( &mut spot_market, - Some(&oracle_data), + Some(oracle_data), clock.unix_timestamp, )?; let token_balance_after_cumulative_interest_update = constituent diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs index f730ba5239..448721acb4 100644 --- a/programs/drift/src/math/lp_pool.rs +++ b/programs/drift/src/math/lp_pool.rs @@ -10,7 +10,7 @@ pub mod perp_lp_pool_settlement { state::{amm_cache::CacheInfo, perp_market::PerpMarket, spot_market::SpotMarket}, *, }; - use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + use anchor_spl::token_interface::{TokenAccount, TokenInterface}; #[derive(Debug, Clone, Copy)] pub struct SettlementResult { @@ -55,7 +55,7 @@ pub mod perp_lp_pool_settlement { ctx: &SettlementContext, result: &SettlementResult, ) -> Result<()> { - if result.amount_transferred > ctx.max_settle_quote_amount as u64 { + if result.amount_transferred > ctx.max_settle_quote_amount { msg!( "Amount to settle exceeds maximum allowed, {} > {}", result.amount_transferred, @@ -94,7 +94,8 @@ pub mod perp_lp_pool_settlement { } fn calculate_perp_to_lp_settlement(ctx: &SettlementContext) -> Result { - let amount_to_send = (ctx.quote_owed_from_lp.abs() as u64).min(ctx.max_settle_quote_amount); + let amount_to_send = + (ctx.quote_owed_from_lp.abs().cast::()?).min(ctx.max_settle_quote_amount); if ctx.fee_pool_balance >= amount_to_send as u128 { // Fee pool can cover entire amount diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index d9ed023897..a3cb5976d0 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -220,7 +220,7 @@ impl LPPool { let out_fee_amount = out_amount .cast::()? - .safe_mul(out_fee as i128)? + .safe_mul(out_fee)? .safe_div(PERCENTAGE_PRECISION_I128)?; Ok((in_amount, out_amount, in_fee_amount, out_fee_amount)) @@ -472,7 +472,7 @@ impl LPPool { let out_spot_market = out_spot_market.unwrap(); let out_oracle_price = out_oracle_price.unwrap(); let out_amount = notional_trade_size - .safe_mul(10_i128.pow(out_spot_market.decimals as u32))? + .safe_mul(10_i128.pow(out_spot_market.decimals))? .safe_div(out_oracle_price.cast::()?)?; ( false, @@ -693,10 +693,10 @@ impl LPPool { if total_quote_owed > 0 { aum = aum - .saturating_sub(total_quote_owed as i128) + .saturating_sub(total_quote_owed) .max(QUOTE_PRECISION_I128); } else if total_quote_owed < 0 { - aum = aum.saturating_add((-total_quote_owed) as i128); + aum = aum.saturating_add(-total_quote_owed); } let aum_u128 = aum.max(0).cast::()?; @@ -739,7 +739,7 @@ impl SpotBalance for ConstituentSpotBalance { } fn balance(&self) -> u128 { - self.scaled_balance as u128 + self.scaled_balance } fn increase_balance(&mut self, delta: u128) -> DriftResult { @@ -883,7 +883,7 @@ impl Constituent { self.pubkey, self.spot_market_index ); - return Err(ErrorCode::InvalidConstituentOperation.into()); + Err(ErrorCode::InvalidConstituentOperation) } else if ConstituentLpOperation::is_operation_paused(self.paused_operations, operation) { msg!( "Constituent {:?}, spot market {}, is paused for operation {:?}", @@ -891,7 +891,7 @@ impl Constituent { self.spot_market_index, operation ); - return Err(ErrorCode::InvalidConstituentOperation.into()); + Err(ErrorCode::InvalidConstituentOperation) } else { Ok(()) } @@ -1175,7 +1175,7 @@ impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { // TODO: validate spot market let datum = self.get(constituent_index as u32); - let target_weight = calculate_target_weight(datum.target_base, &spot_market, price, aum)?; + let target_weight = calculate_target_weight(datum.target_base, spot_market, price, aum)?; Ok(target_weight) } } @@ -1197,7 +1197,7 @@ pub fn calculate_target_weight( .safe_mul(PERCENTAGE_PRECISION_I128)? .safe_div(lp_pool_aum.cast::()?)? .cast::()? - .clamp(-1 * PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_I64); + .clamp(-PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_I64); Ok(target_weight) } @@ -1268,11 +1268,10 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { mapping_index = j; let cell = self.get_mut(i as u32); - let target_base = target_notional + let target_base = -target_notional .safe_div(PERCENTAGE_PRECISION_I128)? .safe_mul(10_i128.pow(decimals as u32))? - .safe_div(price as i128)? - * -1; // Want to target opposite sign of total scaled notional inventory + .safe_div(price as i128)?; // Want to target opposite sign of total scaled notional inventory msg!( "updating constituent index {} target base to {} from aggregated perp notional {}", @@ -1295,7 +1294,7 @@ impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> let mut open_slot_index: Option = None; for i in 0..len { - let cell = self.get(i as u32); + let cell = self.get(i); if cell.constituent_index == datum.constituent_index && cell.perp_market_index == datum.perp_market_index { @@ -1305,7 +1304,7 @@ impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> open_slot_index = Some(i); } } - let open_slot = open_slot_index.ok_or_else(|| ErrorCode::DefaultError.into())?; + let open_slot = open_slot_index.ok_or(ErrorCode::DefaultError)?; let cell = self.get_mut(open_slot); *cell = datum; @@ -1319,11 +1318,11 @@ impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> let len = self.len(); let mut data: Vec = Vec::with_capacity(len as usize); for i in 0..len { - data.push(*self.get(i as u32)); + data.push(*self.get(i)); } data.sort_by_key(|datum| datum.constituent_index); for i in 0..len { - let cell = self.get_mut(i as u32); + let cell = self.get_mut(i); *cell = data[i as usize]; } Ok(()) @@ -1482,8 +1481,8 @@ impl ConstituentCorrelations { "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" )?; - self.correlations[(i as usize * num_constituents + j as usize) as usize] = corr; - self.correlations[(j as usize * num_constituents + i as usize) as usize] = corr; + self.correlations[(i as usize * num_constituents + j as usize)] = corr; + self.correlations[(j as usize * num_constituents + i as usize)] = corr; self.validate()?; @@ -1568,7 +1567,7 @@ pub fn update_constituent_target_base_for_derivatives( constituent_target_base: &mut AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, ) -> DriftResult<()> { for (parent_index, constituent_indexes) in derivative_groups.iter() { - let parent_constituent = constituent_map.get_ref(&(parent_index))?; + let parent_constituent = constituent_map.get_ref(parent_index)?; let parent_target_base = constituent_target_base .get(*parent_index as u32) .target_base; From 631c962ec63ed3efb4b838c7275b8a13079482f8 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:19:32 +0000 Subject: [PATCH 108/159] relax transfer from program lp invariant for devnet --- programs/drift/src/instructions/lp_pool.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 3f7745348c..3fe8588040 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -1600,10 +1600,10 @@ fn transfer_from_program_vault<'info>( // Execute transfer and sync new balance in the constituent account controller::token::send_from_program_vault( - &token_program, - &spot_market_vault, - &constituent_token_account, - &drift_signer, + token_program, + spot_market_vault, + constituent_token_account, + drift_signer, state.signer_nonce, amount, mint, @@ -1648,12 +1648,20 @@ fn transfer_from_program_vault<'info>( .safe_div(PRICE_PRECISION_I64)? }; + #[cfg(feature = "mainnet-beta")] validate!( balance_diff_notional <= PRICE_PRECISION_I64 / 100, ErrorCode::LpInvariantFailed, "Constituent balance mismatch after withdraw from program vault, {}", balance_diff_notional )?; + #[cfg(not(feature = "mainnet-beta"))] + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 10, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault, {}", + balance_diff_notional + )?; Ok(()) } From 8f6f9bc93e5583d06257b80b4e7df583e2e45927 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:40:06 +0000 Subject: [PATCH 109/159] further restrict constituent max borrow --- programs/drift/src/instructions/lp_pool.rs | 11 ++++++++++- programs/drift/src/state/lp_pool.rs | 13 ------------- tests/lpPool.ts | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 3fe8588040..b320651341 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -1588,7 +1588,16 @@ fn transfer_from_program_vault<'info>( let balance_before = constituent.get_full_token_amount(&spot_market)?; - let max_transfer = constituent.get_max_transfer(&spot_market)?; + let max_transfer = constituent + .max_borrow_token_amount + .cast::()? + .safe_add( + constituent + .spot_balance + .get_signed_token_amount(spot_market)?, + )? + .max(0) + .cast::()?; validate!( max_transfer >= amount, diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index a3cb5976d0..8d5c694d61 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -978,19 +978,6 @@ impl Constituent { bytemuck::bytes_of(bump), ] } - - pub fn get_max_transfer(&self, spot_market: &SpotMarket) -> DriftResult { - let token_amount = self.get_full_token_amount(spot_market)?; - let max_transfer = if token_amount < 0 { - self.max_borrow_token_amount - .saturating_sub(token_amount.abs().cast::()?) - } else { - self.max_borrow_token_amount - .saturating_add(token_amount.abs().cast::()?) - }; - - Ok(max_transfer) - } } #[zero_copy] diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 8ba2417fd7..edf682afd4 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -1571,7 +1571,7 @@ describe('LP Pool', () => { ); } catch (e) { console.log(e); - assert(e.toString().includes('0x18b9')); // invariant failed + assert(e.toString().includes('0x18c1')); // invariant failed } }); From ede599bb45fe0083df3ac6cc659fa7a4da652930 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:12:22 +0000 Subject: [PATCH 110/159] give 1% flexilibity for race conditions on max transfer amount --- programs/drift/src/instructions/lp_pool.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index b320651341..24770e3192 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -1588,6 +1588,7 @@ fn transfer_from_program_vault<'info>( let balance_before = constituent.get_full_token_amount(&spot_market)?; + // Adding some 1% flexibility to max threshold to prevent race conditions let max_transfer = constituent .max_borrow_token_amount .cast::()? @@ -1597,6 +1598,8 @@ fn transfer_from_program_vault<'info>( .get_signed_token_amount(spot_market)?, )? .max(0) + .safe_mul(101)? + .safe_div(100)? .cast::()?; validate!( From 60a9231d92f4edacdc8221051e9ea165b037306e Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:23:16 -0700 Subject: [PATCH 111/159] introduce max borrow buffer --- programs/drift/src/instructions/lp_pool.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 24770e3192..bd8e26c69f 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -1588,9 +1588,14 @@ fn transfer_from_program_vault<'info>( let balance_before = constituent.get_full_token_amount(&spot_market)?; - // Adding some 1% flexibility to max threshold to prevent race conditions + // Adding some 5% flexibility to max threshold to prevent race conditions + let buffer = constituent + .max_borrow_token_amount + .safe_mul(5)? + .safe_div(100)?; let max_transfer = constituent .max_borrow_token_amount + .safe_add(buffer)? .cast::()? .safe_add( constituent @@ -1598,14 +1603,12 @@ fn transfer_from_program_vault<'info>( .get_signed_token_amount(spot_market)?, )? .max(0) - .safe_mul(101)? - .safe_div(100)? .cast::()?; validate!( max_transfer >= amount, ErrorCode::LpInvariantFailed, - "Max transfer ({} is less than amount ({})", + "Max transfer ({}) is less than amount ({})", max_transfer, amount )?; From 72a5e42fd00ac58503bc95593f5c69d3408d888d Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:16:22 -0700 Subject: [PATCH 112/159] include new amm inventory limit (#1932) --- programs/drift/src/instructions/lp_admin.rs | 9 +++++++++ programs/drift/src/instructions/lp_pool.rs | 5 ++++- programs/drift/src/state/amm_cache.rs | 10 ++++++---- sdk/src/adminClient.ts | 3 +++ sdk/src/idl/drift.json | 12 +++++++++++- sdk/src/types.ts | 6 +++--- tests/lpPool.ts | 13 +++++++++++++ 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 58f3e18543..2e35be2ffd 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -944,6 +944,7 @@ pub struct OverrideAmmCacheParams { pub last_fee_pool_token_amount: Option, pub last_net_pnl_pool_token_amount: Option, pub amm_position_scalar: Option, + pub amm_inventory_limit: Option, } pub fn handle_override_amm_cache_info<'c: 'info, 'info>( @@ -977,6 +978,14 @@ pub fn handle_override_amm_cache_info<'c: 'info, 'info>( cache_entry.amm_position_scalar = amm_position_scalar; } + if let Some(amm_position_scalar) = override_params.amm_position_scalar { + cache_entry.amm_position_scalar = amm_position_scalar; + } + + if let Some(amm_inventory_limit) = override_params.amm_inventory_limit { + cache_entry.amm_inventory_limit = amm_inventory_limit; + } + Ok(()) } diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index bd8e26c69f..cc01cfcf30 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -134,7 +134,10 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( inventory: cache_info .position .safe_mul(cache_info.amm_position_scalar as i64)? - .safe_div(100)?, + .safe_div(100)? + .abs() + .min(cache_info.amm_inventory_limit) + .safe_mul(cache_info.position.signum())?, price: cache_info.oracle_price, }); } diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index 5b153f7e46..6764cdf5d1 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -50,17 +50,18 @@ pub struct CacheInfo { pub last_settle_slot: u64, pub last_settle_ts: i64, pub quote_owed_from_lp_pool: i64, + pub amm_inventory_limit: i64, pub oracle_price: i64, pub oracle_slot: u64, pub oracle_source: u8, pub oracle_validity: u8, pub lp_status_for_perp_market: u8, pub amm_position_scalar: u8, - pub _padding: [u8; 12], + _padding: [u8; 28], } impl Size for CacheInfo { - const SIZE: usize = 192; + const SIZE: usize = 224; } impl Default for CacheInfo { @@ -80,11 +81,12 @@ impl Default for CacheInfo { last_settle_ts: 0i64, last_settle_amm_pnl: 0i128, last_settle_amm_ex_fees: 0u128, + amm_inventory_limit: 0i64, oracle_source: 0u8, quote_owed_from_lp_pool: 0i64, lp_status_for_perp_market: 0u8, - amm_position_scalar: 100u8, - _padding: [0u8; 12], + amm_position_scalar: 0u8, + _padding: [0u8; 28], } } } diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 45da6a96bd..8042c79fe4 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -715,6 +715,7 @@ export class AdminClient extends DriftClient { lastFeePoolTokenAmount?: BN; lastNetPnlPoolTokenAmount?: BN; ammPositionScalar?: number; + ammInventoryLimit?: BN; }, txParams?: TxParams ): Promise { @@ -737,6 +738,7 @@ export class AdminClient extends DriftClient { lastFeePoolTokenAmount?: BN; lastNetPnlPoolTokenAmount?: BN; ammPositionScalar?: number; + ammInventoryLimit?: BN; } ): Promise { return this.program.instruction.overrideAmmCacheInfo( @@ -749,6 +751,7 @@ export class AdminClient extends DriftClient { lastFeePoolTokenAmount: null, lastNetPnlPoolTokenAmount: null, ammPositionScalar: null, + ammInventoryLimit: null, }, params ), diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 426ec2662a..7e5d95d115 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -12074,6 +12074,12 @@ "type": { "option": "u8" } + }, + { + "name": "ammInventoryLimit", + "type": { + "option": "i64" + } } ] } @@ -12154,6 +12160,10 @@ "name": "quoteOwedFromLpPool", "type": "i64" }, + { + "name": "ammInventoryLimit", + "type": "i64" + }, { "name": "oraclePrice", "type": "i64" @@ -12183,7 +12193,7 @@ "type": { "array": [ "u8", - 12 + 28 ] } } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index ec3fe0ed47..eda35573d6 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1415,9 +1415,9 @@ export interface IWallet { publicKey: PublicKey; payer?: Keypair; supportedTransactionVersions?: - | ReadonlySet - | null - | undefined; + | ReadonlySet + | null + | undefined; } export interface IVersionedWallet { signVersionedTransaction( diff --git a/tests/lpPool.ts b/tests/lpPool.ts index edf682afd4..330ede5f6b 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -722,6 +722,19 @@ describe('LP Pool', () => { ); await adminClient.updateAmmCache([0, 1, 2]); + await adminClient.overrideAmmCacheInfo(0, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + await adminClient.overrideAmmCacheInfo(2, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + let tx = new Transaction(); tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); tx.add( From bc1ae68ce99427b40f7e030483dbfd5645f29dcb Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:05:21 +0000 Subject: [PATCH 113/159] refactor amm cache --- programs/drift/src/instructions/lp_admin.rs | 37 ++++++++++++++++++++- programs/drift/src/lib.rs | 6 ++++ programs/drift/src/state/amm_cache.rs | 6 ++-- sdk/src/idl/drift.json | 28 +++++++++++++++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 2e35be2ffd..6473535f5d 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -4,7 +4,7 @@ use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet}; use crate::instructions::optional_accounts::{get_token_mint, load_maps, AccountMaps}; use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; use crate::math::safe_math::SafeMath; -use crate::state::amm_cache::{AmmCache, AMM_POSITIONS_CACHE}; +use crate::state::amm_cache::{AmmCache, CacheInfo, AMM_POSITIONS_CACHE}; use crate::state::lp_pool::{ AmmConstituentDatum, AmmConstituentMapping, Constituent, ConstituentCorrelations, ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, @@ -937,6 +937,21 @@ pub fn handle_update_initial_amm_cache_info<'c: 'info, 'info>( Ok(()) } + +pub fn handle_reset_amm_cache(ctx: Context) -> Result<()> { + let state = &ctx.accounts.state; + let amm_cache = &mut ctx.accounts.amm_cache; + + amm_cache.cache.clear(); + amm_cache + .cache + .resize_with(state.number_of_markets as usize, CacheInfo::default); + amm_cache.validate(state)?; + + msg!("AMM cache reset. markets: {}", state.number_of_markets); + Ok(()) +} + #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] pub struct OverrideAmmCacheParams { pub quote_owed_from_lp_pool: Option, @@ -1368,3 +1383,23 @@ pub struct UpdateInitialAmmCacheInfo<'info> { )] pub amm_cache: Box>, } + +#[derive(Accounts)] +pub struct ResetAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + realloc = AmmCache::space(state.number_of_markets as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, + pub system_program: Program<'info, System>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 53d05e25b7..cebe39a695 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -2066,6 +2066,12 @@ pub mod drift { handle_override_amm_cache_info(ctx, market_index, override_params) } + pub fn reset_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResetAmmCache<'info>>, + ) -> Result<()> { + handle_reset_amm_cache(ctx) + } + pub fn lp_pool_swap<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, in_market_index: u16, diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index 6764cdf5d1..4cde248bbc 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -57,11 +57,11 @@ pub struct CacheInfo { pub oracle_validity: u8, pub lp_status_for_perp_market: u8, pub amm_position_scalar: u8, - _padding: [u8; 28], + _padding: [u8; 36], } impl Size for CacheInfo { - const SIZE: usize = 224; + const SIZE: usize = 230; } impl Default for CacheInfo { @@ -86,7 +86,7 @@ impl Default for CacheInfo { quote_owed_from_lp_pool: 0i64, lp_status_for_perp_market: 0u8, amm_position_scalar: 0u8, - _padding: [0u8; 28], + _padding: [0u8; 36], } } } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 7e5d95d115..56466d9b28 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -8427,6 +8427,32 @@ } ] }, + { + "name": "resetAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "lpPoolSwap", "accounts": [ @@ -12193,7 +12219,7 @@ "type": { "array": [ "u8", - 28 + 36 ] } } From 6f881ba35b0734a3f793af34f2b909d1b50b435e Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:17:33 +0000 Subject: [PATCH 114/159] change amm cache pda seed for devnet reset --- programs/drift/src/state/amm_cache.rs | 4 ++-- sdk/src/addresses/pda.ts | 2 +- sdk/src/adminClient.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index 4cde248bbc..052778f000 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -22,7 +22,7 @@ use anchor_lang::prelude::*; use super::user::MarketType; -pub const AMM_POSITIONS_CACHE: &str = "amm_cache"; +pub const AMM_POSITIONS_CACHE: &str = "amm_cache_seed"; #[account] #[derive(Debug)] @@ -57,7 +57,7 @@ pub struct CacheInfo { pub oracle_validity: u8, pub lp_status_for_perp_market: u8, pub amm_position_scalar: u8, - _padding: [u8; 36], + pub _padding: [u8; 36], } impl Size for CacheInfo { diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 7100c16c53..0506eb131e 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -508,7 +508,7 @@ export function getConstituentVaultPublicKey( export function getAmmCachePublicKey(programId: PublicKey): PublicKey { return PublicKey.findProgramAddressSync( - [Buffer.from(anchor.utils.bytes.utf8.encode('amm_cache'))], + [Buffer.from(anchor.utils.bytes.utf8.encode('amm_cache_seed'))], programId )[0]; } diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 8042c79fe4..5e2c77a677 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -767,6 +767,32 @@ export class AdminClient extends DriftClient { ); } + public async resetAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getResetAmmCacheIx(); + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getResetAmmCacheIx(): Promise { + return this.program.instruction.resetAmmCache( + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + systemProgram: anchor.web3.SystemProgram.programId, + }, + } + ); + } + public async initializePredictionMarket( perpMarketIndex: number ): Promise { From 8b73b662685894549243e1c8c5baa41ee0c2b6ed Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:34:06 -0700 Subject: [PATCH 115/159] update sdk types file to be up to parity --- sdk/src/adminClient.ts | 22 +++++------ sdk/src/types.ts | 87 +++++++++++++++++++++++++++--------------- 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 5e2c77a677..31b68f4955 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -779,18 +779,16 @@ export class AdminClient extends DriftClient { } public async getResetAmmCacheIx(): Promise { - return this.program.instruction.resetAmmCache( - { - accounts: { - state: await this.getStatePublicKey(), - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, - ammCache: getAmmCachePublicKey(this.program.programId), - systemProgram: anchor.web3.SystemProgram.programId, - }, - } - ); + return this.program.instruction.resetAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); } public async initializePredictionMarket( diff --git a/sdk/src/types.ts b/sdk/src/types.ts index eda35573d6..75c7e12052 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -898,6 +898,7 @@ export type PerpMarketAccount = { lpFeeTransferScalar: number; lpExchangeFeeExcluscionScalar: number; lpStatus: number; + lpPausedOperations: number; }; export type HistoricalOracleData = { @@ -1724,38 +1725,54 @@ export type AmmConstituentDatum = AddAmmConstituentMappingDatum & { }; export type AmmConstituentMapping = { + lpPool: PublicKey; bump: number; weights: AmmConstituentDatum[]; }; export type TargetDatum = { costToTradeBps: number; - beta: number; - targetBase: BN; lastSlot: BN; + targetBase: BN; }; export type ConstituentTargetBaseAccount = { + lpPool: PublicKey; bump: number; targets: TargetDatum[]; }; +export type ConstituentCorrelations = { + lpPool: PublicKey; + bump: number; + correlations: BN[]; +}; + export type LPPoolAccount = { name: number[]; pubkey: PublicKey; mint: PublicKey; + whitelistMint: PublicKey; + constituentTargetBase: PublicKey; + constituentCorrelations: PublicKey; maxAum: BN; lastAum: BN; - lastAumSlot: BN; - lastAumTs: BN; - lastHedgeTs: BN; - bump: number; - totalMintRedeemFeesPaid: BN; cumulativeQuoteSentToPerpMarkets: BN; cumulativeQuoteReceivedFromPerpMarkets: BN; - constituents: number; - whitelistMint: PublicKey; + totalMintRedeemFeesPaid: BN; + lastAumSlot: BN; + maxSettleQuoteAmount: BN; + lastHedgeTs: BN; + mintRedeemId: BN; + settleId: BN; + minMintFee: BN; tokenSupply: BN; + volatility: BN; + constituents: number; + quoteConstituentIndex: number; + bump: number; + gammaExecution: number; + xi: number; }; export type ConstituentSpotBalance = { @@ -1797,49 +1814,59 @@ export enum ConstituentLpOperation { export type ConstituentAccount = { pubkey: PublicKey; - spotMarketIndex: number; - constituentIndex: number; - decimals: number; - bump: number; - constituentDerivativeIndex: number; + mint: PublicKey; + lpPool: PublicKey; + vault: PublicKey; + totalSwapFees: BN; + spotBalance: ConstituentSpotBalance; + lastSpotBalanceTokenAmount: BN; + cumulativeSpotInterestAccruedTokenAmount: BN; maxWeightDeviation: BN; - maxBorrowTokenAmount: BN; swapFeeMin: BN; swapFeeMax: BN; - totalSwapFees: BN; + maxBorrowTokenAmount: BN; vaultTokenBalance: BN; - spotBalance: ConstituentSpotBalance; lastOraclePrice: BN; lastOracleSlot: BN; - mint: PublicKey; oracleStalenessThreshold: BN; - lpPool: PublicKey; - vault: PublicKey; + flashLoanInitialTokenAmount: BN; nextSwapId: BN; derivativeWeight: BN; - flashLoanInitialTokenAmount: BN; + volatility: BN; + constituentDerivativeDepegThreshold: BN; + constituentDerivativeIndex: number; + spotMarketIndex: number; + constituentIndex: number; + decimals: number; + bump: number; + vaultBump: number; + gammaInventory: number; + gammaExecution: number; + xi: number; status: number; pausedOperations: number; }; export type CacheInfo = { - slot: BN; - position: BN; - lastOraclePriceTwap: BN; oracle: PublicKey; - oracleSource: number; - oraclePrice: BN; - oracleSlot: BN; - lastExchangeFees: BN; lastFeePoolTokenAmount: BN; lastNetPnlPoolTokenAmount: BN; + lastExchangeFees: BN; + lastSettleAmmExFees: BN; + lastSettleAmmPnl: BN; + position: BN; + slot: BN; lastSettleAmount: BN; lastSettleSlot: BN; lastSettleTs: BN; - lastSettleAmmPnl: BN; - lastSettleAmmExFees: BN; quoteOwedFromLpPool: BN; + ammInventoryLimit: BN; + oraclePrice: BN; + oracleSlot: BN; + oracleSource: number; + oracleValidity: number; lpStatusForPerpMarket: number; + ammPositionScalar: number; }; export type AmmCache = { From 69afe9089f994b92eecc0516bf323937354f62ac Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:15:13 -0700 Subject: [PATCH 116/159] clean up and guard against negative amm_inventory_limit --- programs/drift/src/instructions/lp_admin.rs | 4 ++++ programs/drift/src/instructions/lp_pool.rs | 18 +++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 6473535f5d..9e430ffbbe 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -998,6 +998,10 @@ pub fn handle_override_amm_cache_info<'c: 'info, 'info>( } if let Some(amm_inventory_limit) = override_params.amm_inventory_limit { + if amm_inventory_limit < 0 { + msg!("amm_inventory_limit must be non-negative"); + return Err(ErrorCode::DefaultError.into()); + } cache_entry.amm_inventory_limit = amm_inventory_limit; } diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index cc01cfcf30..b7a5b1a5aa 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -131,13 +131,17 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( } amm_inventories.push(AmmInventoryAndPrices { - inventory: cache_info - .position - .safe_mul(cache_info.amm_position_scalar as i64)? - .safe_div(100)? - .abs() - .min(cache_info.amm_inventory_limit) - .safe_mul(cache_info.position.signum())?, + inventory: { + let scaled_position = cache_info + .position + .safe_mul(cache_info.amm_position_scalar as i64)? + .safe_div(100)?; + + scaled_position.clamp( + -cache_info.amm_inventory_limit, + cache_info.amm_inventory_limit, + ) + }, price: cache_info.oracle_price, }); } From 0cd3560f92c6ff7b5f4a37d5714267262c8513b6 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:26:51 -0700 Subject: [PATCH 117/159] Target delay increases fees (#1943) * only update the last slot if the oracles and perp positions pass the check per constituent * add in uncertainty fee * add simple test for target delays * change target base seed * prettify --- programs/drift/src/instructions/keeper.rs | 3 +- programs/drift/src/instructions/lp_admin.rs | 4 +- programs/drift/src/instructions/lp_pool.rs | 84 ++++--- programs/drift/src/math/oracle.rs | 4 +- programs/drift/src/state/amm_cache.rs | 22 +- programs/drift/src/state/lp_pool.rs | 151 ++++++++++-- programs/drift/src/state/lp_pool/tests.rs | 243 +++++++++++++++----- sdk/src/addresses/pda.ts | 4 +- 8 files changed, 391 insertions(+), 124 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index fde17c4508..b009c3de90 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -42,7 +42,6 @@ use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; use crate::signer::get_signer_seeds; use crate::state::amm_cache::CacheInfo; -use crate::state::events::emit_stack; use crate::state::events::LPSettleRecord; use crate::state::events::{DeleteUserRecord, OrderActionExplanation, SignedMsgOrderRecord}; use crate::state::fill_mode::FillMode; @@ -3577,7 +3576,7 @@ pub fn handle_update_amm_cache<'c: 'info, 'info>( )?; cached_info.update_perp_market_fields(&perp_market)?; - cached_info.update_oracle_info( + cached_info.try_update_oracle_info( slot, &mm_oracle_price_data, &perp_market, diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 9e430ffbbe..5c406501fe 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -82,7 +82,9 @@ pub fn handle_initialize_lp_pool( gamma_execution: 2, volatility: 4, xi: 2, - padding: 0, + target_oracle_delay_fee_bps_per_10_slots: 0, + target_position_delay_fee_bps_per_10_slots: 0, + padding: [0u8; 15], whitelist_mint, }; diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index b7a5b1a5aa..42a0828037 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -32,8 +32,7 @@ use crate::{ update_constituent_target_base_for_derivatives, AmmConstituentDatum, AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, ConstituentTargetBaseFixed, LPPool, TargetsDatum, LP_POOL_SWAP_AUM_UPDATE_DELAY, - MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC, - MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC, + MAX_ORACLE_STALENESS_FOR_TARGET_CALC, MAX_STALENESS_FOR_TARGET_CALC, }, oracle_map::OracleMap, perp_market_map::MarketSet, @@ -57,7 +56,7 @@ use crate::controller::spot_balance::update_spot_market_cumulative_interest; use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; use crate::instructions::constraints::*; use crate::state::lp_pool::{ - AmmInventoryAndPrices, ConstituentIndexAndDecimalAndPrice, CONSTITUENT_PDA_SEED, + AmmInventoryAndPricesAndSlots, ConstituentIndexAndDecimalAndPrice, CONSTITUENT_PDA_SEED, LP_POOL_TOKEN_VAULT_PDA_SEED, }; @@ -100,37 +99,14 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( let constituent_map = ConstituentMap::load(&ConstituentSet::new(), &lp_pool_key, remaining_accounts)?; - let mut amm_inventories: Vec = + let mut amm_inventories: Vec = Vec::with_capacity(amm_cache.len() as usize); - for (idx, cache_info) in amm_cache.iter().enumerate() { + for (_, cache_info) in amm_cache.iter().enumerate() { if cache_info.lp_status_for_perp_market == 0 { continue; } - if !is_oracle_valid_for_action( - OracleValidity::try_from(cache_info.oracle_validity)?, - Some(DriftAction::UpdateLpConstituentTargetBase), - )? { - msg!( - "Oracle data for perp market {} is invalid. Skipping update", - idx, - ); - continue; - } - - if slot.safe_sub(cache_info.slot)? > MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC { - msg!("Amm cache for perp market {}. Skipping update", idx); - continue; - } - - if slot.safe_sub(cache_info.oracle_slot)? > MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC { - msg!( - "Amm cache oracle for perp market {} is stale. Skipping update", - idx - ); - continue; - } - amm_inventories.push(AmmInventoryAndPrices { + amm_inventories.push(AmmInventoryAndPricesAndSlots { inventory: { let scaled_position = cache_info .position @@ -143,6 +119,8 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( ) }, price: cache_info.oracle_price, + last_oracle_slot: cache_info.oracle_slot, + last_position_slot: cache_info.slot, }); } msg!("amm inventories: {:?}", amm_inventories); @@ -400,8 +378,18 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( out_oracle.price, lp_pool.last_aum, )?; + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + in_target_position_slot_delay, + out_target_position_slot_delay, + in_target_oracle_slot_delay, + out_target_oracle_slot_delay, &in_oracle, &out_oracle, &in_constituent, @@ -541,6 +529,9 @@ pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = ctx.accounts.constituent_correlations.load_zc()?; + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + let AccountMaps { perp_market_map: _, spot_market_map, @@ -580,7 +571,18 @@ pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( 0, )?; + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + in_target_position_slot_delay, + out_target_position_slot_delay, + in_target_oracle_slot_delay, + out_target_oracle_slot_delay, &in_oracle, &out_oracle, &in_constituent, @@ -722,8 +724,14 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( let dlp_total_supply = ctx.accounts.lp_mint.supply; + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool .get_add_liquidity_mint_amount( + in_target_position_slot_delay, + in_target_oracle_slot_delay, &in_spot_market, &in_constituent, in_amount, @@ -930,8 +938,14 @@ pub fn handle_view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( let dlp_total_supply = ctx.accounts.lp_mint.supply; + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool .get_add_liquidity_mint_amount( + in_target_position_slot_delay, + in_target_oracle_slot_delay, &in_spot_market, &in_constituent, in_amount, @@ -1069,8 +1083,14 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( let dlp_total_supply = ctx.accounts.lp_mint.supply; + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool .get_remove_liquidity_amount( + out_target_position_slot_delay, + out_target_oracle_slot_delay, &out_spot_market, &out_constituent, lp_to_burn, @@ -1315,8 +1335,14 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( let dlp_total_supply = ctx.accounts.lp_mint.supply; + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool .get_remove_liquidity_amount( + out_target_position_slot_delay, + out_target_oracle_slot_delay, &out_spot_market, &out_constituent, lp_to_burn, diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index d569faf718..40314ec000 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -102,7 +102,7 @@ pub enum DriftAction { UpdateAMMCurve, OracleOrderPrice, UseMMOraclePrice, - UpdateLpConstituentTargetBase, + UpdateAmmCache, UpdateLpPoolAum, LpPoolSwap, } @@ -153,7 +153,7 @@ pub fn is_oracle_valid_for_action( | OracleValidity::StaleForMargin ), DriftAction::FillOrderMatch - | DriftAction::UpdateLpConstituentTargetBase + | DriftAction::UpdateAmmCache | DriftAction::UpdateLpPoolAum | DriftAction::LpPoolSwap => !matches!( oracle_validity, diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index 052778f000..df00f98ec0 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -3,7 +3,7 @@ use std::convert::TryFrom; use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::calculate_net_user_pnl; use crate::math::casting::Cast; -use crate::math::oracle::{oracle_validity, LogMode}; +use crate::math::oracle::{is_oracle_valid_for_action, oracle_validity, DriftAction, LogMode}; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; use crate::state::oracle::MMOraclePriceData; @@ -120,7 +120,7 @@ impl CacheInfo { Ok(()) } - pub fn update_oracle_info( + pub fn try_update_oracle_info( &mut self, clock_slot: u64, oracle_price_data: &MMOraclePriceData, @@ -128,9 +128,6 @@ impl CacheInfo { oracle_guard_rails: &OracleGuardRails, ) -> DriftResult<()> { let safe_oracle_data = oracle_price_data.get_safe_oracle_price_data(); - self.oracle_price = safe_oracle_data.price; - self.oracle_slot = clock_slot.safe_sub(safe_oracle_data.delay.max(0) as u64)?; - self.slot = clock_slot; let validity = oracle_validity( MarketType::Perp, perp_market.market_index, @@ -145,7 +142,18 @@ impl CacheInfo { LogMode::SafeMMOracle, perp_market.amm.oracle_slot_delay_override, )?; - self.oracle_validity = u8::from(validity); + if is_oracle_valid_for_action(validity, Some(DriftAction::UpdateAmmCache))? { + self.oracle_price = safe_oracle_data.price; + self.oracle_slot = clock_slot.safe_sub(safe_oracle_data.delay.max(0) as u64)?; + self.oracle_validity = u8::from(validity); + } else { + msg!( + "Not updating oracle price for perp market {}. Oracle data is invalid", + perp_market.market_index + ); + } + self.slot = clock_slot; + Ok(()) } } @@ -204,7 +212,7 @@ impl AmmCache { ) -> DriftResult<()> { let cache_info = self.cache.get_mut(market_index as usize); if let Some(cache_info) = cache_info { - cache_info.update_oracle_info( + cache_info.try_update_oracle_info( clock_slot, oracle_price_data, perp_market, diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 8d5c694d61..667e8da54d 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -27,7 +27,7 @@ use crate::{impl_zero_copy_loader, validate}; pub const LP_POOL_PDA_SEED: &str = "lp_pool"; pub const AMM_MAP_PDA_SEED: &str = "AMM_MAP"; pub const CONSTITUENT_PDA_SEED: &str = "CONSTITUENT"; -pub const CONSTITUENT_TARGET_BASE_PDA_SEED: &str = "constituent_target_base"; +pub const CONSTITUENT_TARGET_BASE_PDA_SEED: &str = "constituent_target_base_seed"; pub const CONSTITUENT_CORRELATIONS_PDA_SEED: &str = "constituent_correlations"; pub const CONSTITUENT_VAULT_PDA_SEED: &str = "CONSTITUENT_VAULT"; pub const LP_POOL_TOKEN_VAULT_PDA_SEED: &str = "LP_POOL_TOKEN_VAULT"; @@ -45,14 +45,14 @@ pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 100; pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 10; pub const LP_POOL_SWAP_AUM_UPDATE_DELAY: u64 = 0; #[cfg(feature = "anchor-test")] -pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +pub const MAX_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; #[cfg(not(feature = "anchor-test"))] -pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 0u64; +pub const MAX_STALENESS_FOR_TARGET_CALC: u64 = 0u64; #[cfg(feature = "anchor-test")] -pub const MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +pub const MAX_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; #[cfg(not(feature = "anchor-test"))] -pub const MAX_AMM_CACHE_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10u64; +pub const MAX_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10u64; #[cfg(test)] mod tests; @@ -105,6 +105,7 @@ pub struct LPPool { /// PERCENTAGE_PRECISION pub min_mint_fee: i64, + pub token_supply: u64, // PERCENTAGE_PRECISION: percentage precision const = 100% @@ -120,7 +121,11 @@ pub struct LPPool { // No precision - just constant pub xi: u8, - pub padding: u8, + // Bps of fees for target delays + pub target_oracle_delay_fee_bps_per_10_slots: u8, + pub target_position_delay_fee_bps_per_10_slots: u8, + + pub padding: [u8; 15], } impl Size for LPPool { @@ -176,6 +181,10 @@ impl LPPool { /// Returns (in_amount out_amount, in_fee, out_fee) pub fn get_swap_amount( &self, + in_target_position_slot_delay: u64, + out_target_position_slot_delay: u64, + in_target_oracle_slot_delay: u64, + out_target_oracle_slot_delay: u64, in_oracle: &OraclePriceData, out_oracle: &OraclePriceData, in_constituent: &Constituent, @@ -194,7 +203,7 @@ impl LPPool { out_oracle, )?; - let (in_fee, out_fee) = self.get_swap_fees( + let (mut in_fee, mut out_fee) = self.get_swap_fees( in_spot_market, in_oracle.price, in_constituent, @@ -206,6 +215,16 @@ impl LPPool { Some(out_target_weight), correlation, )?; + + in_fee += self.get_target_uncertainty_fees( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + )?; + out_fee += self.get_target_uncertainty_fees( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + )?; + let in_fee_amount = in_amount .cast::()? .safe_mul(in_fee)? @@ -230,6 +249,8 @@ impl LPPool { /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision pub fn get_add_liquidity_mint_amount( &self, + in_target_position_slot_delay: u64, + in_target_oracle_slot_delay: u64, in_spot_market: &SpotMarket, in_constituent: &Constituent, in_amount: u128, @@ -237,7 +258,7 @@ impl LPPool { in_target_weight: i64, dlp_total_supply: u64, ) -> DriftResult<(u64, u128, i64, i128)> { - let (in_fee_pct, out_fee_pct) = if self.last_aum == 0 { + let (mut in_fee_pct, out_fee_pct) = if self.last_aum == 0 { (0, 0) } else { self.get_swap_fees( @@ -253,7 +274,12 @@ impl LPPool { 0, )? }; - let in_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + in_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + in_fee_pct += self.get_target_uncertainty_fees( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + )?; + let in_fee_amount = in_amount .cast::()? .safe_mul(in_fee_pct)? @@ -298,6 +324,8 @@ impl LPPool { /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision pub fn get_remove_liquidity_amount( &self, + out_target_position_slot_delay: u64, + out_target_oracle_slot_delay: u64, out_spot_market: &SpotMarket, out_constituent: &Constituent, lp_to_burn: u64, @@ -337,7 +365,7 @@ impl LPPool { .safe_div(10u128.pow(3))? .safe_div(out_oracle.price.cast::()?)?; - let (in_fee_pct, out_fee_pct) = self.get_swap_fees( + let (in_fee_pct, mut out_fee_pct) = self.get_swap_fees( out_spot_market, out_oracle.price, out_constituent, @@ -349,7 +377,12 @@ impl LPPool { None, 0, )?; - let out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + + out_fee_pct += self.get_target_uncertainty_fees( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + )?; + out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; let out_fee_amount = out_amount .cast::()? .safe_mul(out_fee_pct)? @@ -602,6 +635,45 @@ impl LPPool { )) } + pub fn get_target_uncertainty_fees( + self, + target_position_slot_delay: u64, + target_oracle_slot_delay: u64, + ) -> DriftResult { + // Gives an uncertainty fee in bps if the oracle or position was stale when calcing target. + // Uses a step function that goes up every 10 slots beyond a threshold where we consider it okay + // - delay 0 (<= threshold) = 0 bps + // - delay 1..10 = 10 bps (1 block) + // - delay 11..20 = 20 bps (2 blocks) + fn step_fee(delay: u64, threshold: u64, per_10_slot_bps: u8) -> DriftResult { + if delay <= threshold || per_10_slot_bps == 0 { + return Ok(0); + } + let elapsed = delay.saturating_sub(threshold); + let blocks = (elapsed + 9) / 10; + let fee_bps = (blocks as u128).safe_mul(per_10_slot_bps as u128)?; + let fee = fee_bps + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(10_000u128)?; + Ok(fee) + } + + let oracle_uncertainty_fee = step_fee( + target_oracle_slot_delay, + MAX_ORACLE_STALENESS_FOR_TARGET_CALC, + self.target_oracle_delay_fee_bps_per_10_slots, + )?; + let position_uncertainty_fee = step_fee( + target_position_slot_delay, + MAX_STALENESS_FOR_TARGET_CALC, + self.target_position_delay_fee_bps_per_10_slots, + )?; + + Ok(oracle_uncertainty_fee + .safe_add(position_uncertainty_fee)? + .cast::()?) + } + pub fn record_mint_redeem_fees(&mut self, amount: i64) -> DriftResult { self.total_mint_redeem_fees_paid = self .total_mint_redeem_fees_paid @@ -808,6 +880,7 @@ pub struct Constituent { pub last_oracle_price: i64, pub last_oracle_slot: u64, + /// Delay allowed for valid AUM calculation pub oracle_staleness_threshold: u64, pub flash_loan_initial_token_amount: u64, @@ -1064,8 +1137,9 @@ impl_zero_copy_loader!( pub struct TargetsDatum { pub cost_to_trade_bps: i32, pub _padding: [u8; 4], - pub last_slot: u64, pub target_base: i64, + pub last_oracle_slot: u64, + pub last_position_slot: u64, } #[zero_copy] @@ -1098,7 +1172,7 @@ pub struct ConstituentTargetBase { impl ConstituentTargetBase { pub fn space(num_constituents: usize) -> usize { - 8 + 40 + num_constituents * 24 + 8 + 40 + num_constituents * 32 } pub fn validate(&self) -> DriftResult<()> { @@ -1191,9 +1265,11 @@ pub fn calculate_target_weight( /// Update target base based on amm_inventory and mapping #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AmmInventoryAndPrices { +pub struct AmmInventoryAndPricesAndSlots { pub inventory: i64, pub price: i64, + pub last_oracle_slot: u64, + pub last_position_slot: u64, } pub struct ConstituentIndexAndDecimalAndPrice { @@ -1206,7 +1282,7 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { pub fn update_target_base( &mut self, mapping: &AccountZeroCopy<'a, AmmConstituentDatum, AmmConstituentMappingFixed>, - amm_inventory_and_prices: &[AmmInventoryAndPrices], + amm_inventory_and_prices: &[AmmInventoryAndPricesAndSlots], constituents_indexes_and_decimals_and_prices: &mut [ConstituentIndexAndDecimalAndPrice], slot: u64, ) -> DriftResult<()> { @@ -1214,12 +1290,19 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { constituents_indexes_and_decimals_and_prices.sort_by_key(|c| c.constituent_index); // Precompute notional by perp market index - let mut notionals: Vec = Vec::with_capacity(amm_inventory_and_prices.len()); - for &AmmInventoryAndPrices { inventory, price } in amm_inventory_and_prices.iter() { + let mut notionals_and_slots: Vec<(i128, u64, u64)> = + Vec::with_capacity(amm_inventory_and_prices.len()); + for &AmmInventoryAndPricesAndSlots { + inventory, + price, + last_oracle_slot, + last_position_slot, + } in amm_inventory_and_prices.iter() + { let notional = (inventory as i128) .safe_mul(price as i128)? .safe_div(BASE_PRECISION_I128)?; - notionals.push(notional); + notionals_and_slots.push((notional, last_oracle_slot, last_position_slot)); } let mut mapping_index = 0; @@ -1237,6 +1320,8 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { let mut target_notional = 0i128; let mut j = mapping_index; + let mut oldest_oracle_slot = u64::MAX; + let mut oldest_position_slot = u64::MAX; while j < mapping.len() { let d = mapping.get(j); if d.constituent_index != constituent_index { @@ -1246,9 +1331,14 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { } break; } - if let Some(perp_notional) = notionals.get(d.perp_market_index as usize) { + if let Some((perp_notional, perp_last_oracle_slot, perp_last_position_slot)) = + notionals_and_slots.get(d.perp_market_index as usize) + { target_notional = target_notional .saturating_add(perp_notional.saturating_mul(d.weight as i128)); + + oldest_oracle_slot = oldest_oracle_slot.min(*perp_last_oracle_slot); + oldest_position_slot = oldest_position_slot.min(*perp_last_position_slot); } j += 1; } @@ -1267,7 +1357,28 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { target_notional, ); cell.target_base = target_base.cast::()?; - cell.last_slot = slot; + + if slot.saturating_sub(oldest_position_slot) <= MAX_STALENESS_FOR_TARGET_CALC { + cell.last_position_slot = slot; + } else { + msg!( + "not updating last_position_slot for target base constituent_index {}: oldest_position_slot {}, current slot {}", + constituent_index, + oldest_position_slot, + slot + ); + } + + if slot.saturating_sub(oldest_oracle_slot) <= MAX_ORACLE_STALENESS_FOR_TARGET_CALC { + cell.last_oracle_slot = slot; + } else { + msg!( + "not updating last_oracle_slot for target base constituent_index {}: oldest_oracle_slot {}, current slot {}", + constituent_index, + oldest_oracle_slot, + slot + ); + } } Ok(()) diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index 8a7638cc24..a460bcd151 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -68,22 +68,30 @@ mod tests { } }; - let amm_inventory_and_price: Vec = vec![ - AmmInventoryAndPrices { + let amm_inventory_and_price: Vec = vec![ + AmmInventoryAndPricesAndSlots { inventory: 4 * BASE_PRECISION_I64, price: 100_000 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, }, // $400k BTC - AmmInventoryAndPrices { + AmmInventoryAndPricesAndSlots { inventory: 2000 * BASE_PRECISION_I64, price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, }, // $400k SOL - AmmInventoryAndPrices { + AmmInventoryAndPricesAndSlots { inventory: 200 * BASE_PRECISION_I64, price: 1500 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, }, // $300k ETH - AmmInventoryAndPrices { + AmmInventoryAndPricesAndSlots { inventory: 16500 * BASE_PRECISION_I64, price: PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, }, // $16.5k FARTCOIN ]; let mut constituents_indexes_and_decimals_and_prices = vec![ @@ -114,8 +122,7 @@ mod tests { len: 4, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 96]); - let now_ts = 1234567890; + let target_data = RefCell::new([0u8; 4 * 32]); let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), data: target_data.borrow_mut(), @@ -127,7 +134,7 @@ mod tests { &mapping_zc, &amm_inventory_and_price, constituents_indexes_and_decimals_and_prices.as_mut_slice(), - now_ts, + slot, ) .unwrap(); @@ -155,6 +162,7 @@ mod tests { #[test] fn test_single_zero_weight() { + let slot = 20202020 as u64; let amm_datum = amm_const_datum(0, 1, 0, 0); let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { len: 1, @@ -181,23 +189,25 @@ mod tests { } }; - let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { - inventory: 1_000_000, - price: 1_000_000, - }]; + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000, + price: 1_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; let mut constituents_indexes_and_decimals_and_prices = vec![ConstituentIndexAndDecimalAndPrice { constituent_index: 1, decimals: 6, price: 1_000_000, }]; - let now_ts = 1000; let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 1, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 24]); + let target_data = RefCell::new([0u8; 32]); let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), data: target_data.borrow_mut(), @@ -209,18 +219,19 @@ mod tests { &mapping_zc, &amm_inventory_and_prices, constituents_indexes_and_decimals_and_prices.as_mut_slice(), - now_ts, + slot, ) .unwrap(); assert!(target_zc_mut.iter().all(|&x| x.target_base == 0)); assert_eq!(target_zc_mut.len(), 1); assert_eq!(target_zc_mut.get(0).target_base, 0); - assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); } #[test] fn test_single_full_weight() { + let slot = 20202020 as u64; let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { len: 1, @@ -248,10 +259,13 @@ mod tests { }; let price = PRICE_PRECISION_I64; - let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { - inventory: BASE_PRECISION_I64, - price, - }]; + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: BASE_PRECISION_I64, + price, + last_oracle_slot: slot, + last_position_slot: slot, + }]; let mut constituents_indexes_and_decimals_and_prices = vec![ConstituentIndexAndDecimalAndPrice { constituent_index: 1, @@ -265,19 +279,20 @@ mod tests { len: 1, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 24]); + let target_data = RefCell::new([0u8; 32]); let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), data: target_data.borrow_mut(), _marker: PhantomData::, }; + // Should succeed but not update the target's slot if clock slot is too recent target_zc_mut .update_target_base( &mapping_zc, &amm_inventory_and_prices, constituents_indexes_and_decimals_and_prices.as_mut_slice(), - now_ts, + 4040404040440404404, // far future slot ) .unwrap(); @@ -295,11 +310,22 @@ mod tests { ); assert_eq!(weight, -1000000); assert_eq!(target_zc_mut.len(), 1); - assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, 0); + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); // still not updated } #[test] fn test_multiple_constituents_partial_weights() { + let slot = 20202020 as u64; let amm_mapping_data = vec![ amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64 / 2, 111), amm_const_datum(0, 2, PERCENTAGE_PRECISION_I64 / 2, 111), @@ -337,10 +363,13 @@ mod tests { } }; - let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { - inventory: 1_000_000_000, - price: 1_000_000, - }]; + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000_000, + price: 1_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; let mut constituents_indexes_and_decimals_and_prices = vec![ ConstituentIndexAndDecimalAndPrice { constituent_index: 1, @@ -361,7 +390,7 @@ mod tests { len: amm_mapping_data.len() as u32, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 48]); + let target_data = RefCell::new([0u8; 2 * 32]); let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), data: target_data.borrow_mut(), @@ -373,7 +402,7 @@ mod tests { &mapping_zc, &amm_inventory_and_prices, constituents_indexes_and_decimals_and_prices.as_mut_slice(), - now_ts, + slot, ) .unwrap(); @@ -393,12 +422,13 @@ mod tests { .unwrap(), -1 * PERCENTAGE_PRECISION_I64 / 2 ); - assert_eq!(target_zc_mut.get(i).last_slot, now_ts); + assert_eq!(target_zc_mut.get(i).last_oracle_slot, slot); } } #[test] fn test_zero_aum_safe() { + let slot = 20202020 as u64; let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { len: 1, @@ -425,10 +455,13 @@ mod tests { } }; - let amm_inventory_and_prices: Vec = vec![AmmInventoryAndPrices { - inventory: 1_000_000, - price: 142_000_000, - }]; + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000, + price: 142_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; let mut constituents_indexes_and_decimals_and_prices = vec![ConstituentIndexAndDecimalAndPrice { constituent_index: 1, @@ -442,7 +475,7 @@ mod tests { len: 1, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 24]); + let target_data = RefCell::new([0u8; 32]); let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), data: target_data.borrow_mut(), @@ -454,18 +487,20 @@ mod tests { &mapping_zc, &amm_inventory_and_prices, constituents_indexes_and_decimals_and_prices.as_mut_slice(), - now_ts, + slot, ) .unwrap(); assert_eq!(target_zc_mut.len(), 1); assert_eq!(target_zc_mut.get(0).target_base, -1_000_000); // despite no aum, desire to reach target - assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); } } #[cfg(test)] mod swap_tests { + use core::slice; + use crate::math::constants::{ PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I128, PRICE_PRECISION_I64, SPOT_BALANCE_PRECISION, @@ -500,6 +535,10 @@ mod swap_tests { } fn get_swap_amount_decimals_scenario( + in_target_position_delay: u64, + out_target_position_delay: u64, + in_target_oracle_delay: u64, + out_target_oracle_delay: u64, in_current_weight: u64, out_current_weight: u64, in_decimals: u32, @@ -520,6 +559,8 @@ mod swap_tests { ) { let lp_pool = LPPool { last_aum: 1_000_000_000_000, + target_oracle_delay_fee_bps_per_10_slots: 2, + target_position_delay_fee_bps_per_10_slots: 10, ..LPPool::default() }; @@ -573,6 +614,10 @@ mod swap_tests { let (in_amount, out_amount, in_fee, out_fee) = lp_pool .get_swap_amount( + in_target_position_delay, + out_target_position_delay, + in_target_oracle_delay, + out_target_oracle_delay, &oracle_0, &oracle_1, &constituent_0, @@ -594,6 +639,10 @@ mod swap_tests { #[test] fn test_get_swap_amount_in_6_out_6() { get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, 500_000, 500_000, 6, @@ -617,6 +666,10 @@ mod swap_tests { #[test] fn test_get_swap_amount_in_6_out_9() { get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, 500_000, 500_000, 6, @@ -640,6 +693,10 @@ mod swap_tests { #[test] fn test_get_swap_amount_in_9_out_6() { get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, 500_000, 500_000, 9, @@ -714,6 +771,8 @@ mod swap_tests { } fn get_add_liquidity_mint_amount_scenario( + in_target_position_delay: u64, + in_target_oracle_delay: u64, last_aum: u128, now: i64, in_decimals: u32, @@ -773,6 +832,8 @@ mod swap_tests { let (lp_amount, in_amount_1, lp_fee, in_fee_amount) = lp_pool .get_add_liquidity_mint_amount( + in_target_position_delay, + in_target_oracle_delay, &spot_market, &constituent, in_amount, @@ -792,7 +853,7 @@ mod swap_tests { #[test] fn test_get_add_liquidity_mint_amount_zero_aum() { get_add_liquidity_mint_amount_scenario( - 0, // last_aum + 0, 0, 0, // last_aum 0, // now 6, // in_decimals 1_000_000, // in_amount @@ -807,6 +868,8 @@ mod swap_tests { #[test] fn test_get_add_liquidity_mint_amount_with_existing_aum() { get_add_liquidity_mint_amount_scenario( + 0, + 0, 10_000_000_000, // last_aum ($10,000) 0, // now 6, // in_decimals @@ -826,6 +889,8 @@ mod swap_tests { #[test] fn test_get_add_liquidity_mint_amount_with_zero_aum_8_decimals() { get_add_liquidity_mint_amount_scenario( + 0, + 0, 0, // last_aum 0, // now 8, // in_decimals @@ -844,6 +909,8 @@ mod swap_tests { #[test] fn test_get_add_liquidity_mint_amount_with_existing_aum_8_decimals() { get_add_liquidity_mint_amount_scenario( + 0, + 0, 10_000_000_000, // last_aum ($10,000) 0, // now 8, // in_decimals @@ -863,7 +930,7 @@ mod swap_tests { #[test] fn test_get_add_liquidity_mint_amount_with_zero_aum_4_decimals() { get_add_liquidity_mint_amount_scenario( - 0, // last_aum + 0, 0, 0, // last_aum 0, // now 4, // in_decimals 10_000, // in_amount (1 token) = $1 @@ -878,6 +945,8 @@ mod swap_tests { #[test] fn test_get_add_liquidity_mint_amount_with_existing_aum_4_decimals() { get_add_liquidity_mint_amount_scenario( + 0, + 0, 10_000_000_000, // last_aum ($10,000) 0, // now 4, // in_decimals @@ -894,6 +963,8 @@ mod swap_tests { } fn get_remove_liquidity_mint_amount_scenario( + out_target_position_delay: u64, + out_target_oracle_delay: u64, last_aum: u128, now: i64, in_decimals: u32, @@ -953,6 +1024,8 @@ mod swap_tests { let (lp_amount_1, out_amount, lp_fee, out_fee_amount) = lp_pool .get_remove_liquidity_amount( + out_target_position_delay, + out_target_position_delay, &spot_market, &constituent, lp_burn_amount, @@ -972,6 +1045,8 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum() { get_remove_liquidity_mint_amount_scenario( + 0, + 0, 10_000_000_000, // last_aum ($10,000) 0, // now 6, // in_decimals @@ -991,6 +1066,8 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals() { get_remove_liquidity_mint_amount_scenario( + 0, + 0, 10_000_000_000, // last_aum ($10,000) 0, // now 8, // in_decimals @@ -1011,6 +1088,8 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum_4_decimals() { get_remove_liquidity_mint_amount_scenario( + 0, + 0, 10_000_000_000, // last_aum ($10,000) 0, // now 4, // in_decimals @@ -1029,6 +1108,8 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum_5_decimals_large_aum() { get_remove_liquidity_mint_amount_scenario( + 0, + 0, 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) 0, // now 5, // in_decimals @@ -1047,14 +1128,16 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum_6_decimals_large_aum() { get_remove_liquidity_mint_amount_scenario( - 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) - 0, // now - 6, // in_decimals + 0, + 0, + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 6, // in_decimals 100_000_000_000 * 1_000_000 - 1_000_000, // Leave in QUOTE AMOUNT - 100_000_000_000 * 1_000_000, // dlp_total_supply - 99989999900000000, // expected_out_amount - 9999999999900, // expected_lp_fee - 349765019650200, // expected_out_fee_amount + 100_000_000_000 * 1_000_000, // dlp_total_supply + 99989999900000000, // expected_out_amount + 9999999999900, // expected_lp_fee + 349765019650200, // expected_out_fee_amount 1, 2, 2, @@ -1065,14 +1148,16 @@ mod swap_tests { #[test] fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals_large_aum() { get_remove_liquidity_mint_amount_scenario( - 10_000_000_000_000_000, // last_aum ($10,000,000,000) - 0, // now - 8, // in_decimals - 10_000_000_000 * 1_000_000 - 1_000_000, // in_amount - 10_000_000_000 * 1_000_000, // dlp_total_supply - 999899999000000000, // expected_out_amount + 0, + 0, + 10_000_000_000_000_000, // last_aum ($10,000,000,000) + 0, // now + 8, // in_decimals + 10_000_000_000 * 1_000_000 - 1_000_000, // in_amount + 10_000_000_000 * 1_000_000, // dlp_total_supply + 999899999000000000, // expected_out_amount (10_000_000_000 * 1_000_000 - 1_000_000) / 10000, // expected_lp_fee - 3497650196502000, // expected_out_fee_amount + 3497650196502000, // expected_out_fee_amount 1, 2, 2, @@ -1090,6 +1175,10 @@ mod swap_tests { } fn get_swap_amounts( + in_target_position_delay: u64, + out_target_position_delay: u64, + in_target_oracle_delay: u64, + out_target_oracle_delay: u64, in_oracle_price: i64, out_oracle_price: i64, in_current_weight: i64, @@ -1189,6 +1278,10 @@ mod swap_tests { let (in_amount_result, out_amount, in_fee, out_fee) = lp_pool .get_swap_amount( + in_target_position_delay, + out_target_position_delay, + in_target_oracle_delay, + out_target_oracle_delay, &oracle_0, &oracle_1, &constituent_0, @@ -1245,6 +1338,10 @@ mod swap_tests { in_token_amount_pre, out_token_amount_pre, ) = get_swap_amounts( + 0, + 0, + 0, + 0, in_oracle_price, out_oracle_price, *in_current_weight, @@ -1328,6 +1425,10 @@ mod swap_tests { in_token_amount_pre, out_token_amount_pre, ) = get_swap_amounts( + 0, + 0, + 0, + 0, in_oracle_price, out_oracle_price, *in_current_weight, @@ -1536,6 +1637,24 @@ mod swap_fee_tests { assert_eq!(fee_in, 6 * PERCENTAGE_PRECISION_I128 / 100000); // 0.6 bps assert_eq!(fee_out, -6 * PERCENTAGE_PRECISION_I128 / 100000); // -0.6 bps } + + #[test] + fn test_target_delays() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + target_oracle_delay_fee_bps_per_10_slots: 2, + target_position_delay_fee_bps_per_10_slots: 10, + ..LPPool::default() + }; + + // Even a small delay in the position incurs a larger fee + let uncertainty_fee = lp_pool.get_target_uncertainty_fees(1, 0).unwrap(); + assert_eq!( + uncertainty_fee, + PERCENTAGE_PRECISION_I128 / 10000i128 + * lp_pool.target_position_delay_fee_bps_per_10_slots as i128 + ); + } } #[cfg(test)] @@ -2398,7 +2517,7 @@ mod update_aum_tests { len: 4, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 96]); // 4 * 24 bytes per TargetsDatum + let target_data = RefCell::new([0u8; 128]); // 4 * 32 bytes per TargetsDatum let mut constituent_target_base = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), @@ -2733,7 +2852,7 @@ mod update_constituent_target_base_for_derivatives_tests { len: num_constituents as u32, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 120]); // 4+1 constituents * 24 bytes per TargetsDatum + let target_data = RefCell::new([0u8; 5 * 32]); // 4+1 constituents * 32 bytes per TargetsDatum let mut constituent_target_base = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), @@ -2749,7 +2868,7 @@ mod update_constituent_target_base_for_derivatives_tests { .target_base = initial_parent_target_base; constituent_target_base .get_mut(parent_index as u32) - .last_slot = 100; + .last_oracle_slot = 100; // Initialize derivative target bases to 0 constituent_target_base @@ -2757,19 +2876,19 @@ mod update_constituent_target_base_for_derivatives_tests { .target_base = 0; constituent_target_base .get_mut(derivative1_index as u32) - .last_slot = 100; + .last_oracle_slot = 100; constituent_target_base .get_mut(derivative2_index as u32) .target_base = 0; constituent_target_base .get_mut(derivative2_index as u32) - .last_slot = 100; + .last_oracle_slot = 100; constituent_target_base .get_mut(derivative3_index as u32) .target_base = 0; constituent_target_base .get_mut(derivative3_index as u32) - .last_slot = 100; + .last_oracle_slot = 100; // Create derivative groups let mut derivative_groups = BTreeMap::new(); @@ -2971,7 +3090,7 @@ mod update_constituent_target_base_for_derivatives_tests { len: 2, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 72]); // 2+1 constituents * 24 bytes per TargetsDatum + let target_data = RefCell::new([0u8; 3 * 32]); // 2+1 constituents * 24 bytes per TargetsDatum let mut constituent_target_base = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), @@ -3212,7 +3331,7 @@ mod update_constituent_target_base_for_derivatives_tests { len: 2, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 72]); + let target_data = RefCell::new([0u8; 3 * 32]); let mut constituent_target_base = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), @@ -3380,7 +3499,7 @@ mod update_constituent_target_base_for_derivatives_tests { len: 3, ..ConstituentTargetBaseFixed::default() }); - let target_data = RefCell::new([0u8; 96]); + let target_data = RefCell::new([0u8; 4 * 32]); let mut constituent_target_base = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 0506eb131e..43070e6dcf 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -469,7 +469,9 @@ export function getConstituentTargetBasePublicKey( ): PublicKey { return PublicKey.findProgramAddressSync( [ - Buffer.from(anchor.utils.bytes.utf8.encode('constituent_target_base')), + Buffer.from( + anchor.utils.bytes.utf8.encode('constituent_target_base_seed') + ), lpPoolPublicKey.toBuffer(), ], programId From 84ce292b6435715b86cbead04a7b832c35c54672 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:47:05 -0700 Subject: [PATCH 118/159] update idl --- sdk/src/idl/drift.json | 35 ++++++++++++++++++++++++++--------- sdk/src/types.ts | 3 ++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index f665014668..3b27efcca6 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -9890,8 +9890,21 @@ "type": "u8" }, { - "name": "padding", + "name": "targetOracleDelayFeeBpsPer10Slots", + "type": "u8" + }, + { + "name": "targetPositionDelayFeeBpsPer10Slots", "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 15 + ] + } } ] } @@ -9993,6 +10006,9 @@ }, { "name": "oracleStalenessThreshold", + "docs": [ + "Delay allowed for valid AUM calculation" + ], "type": "u64" }, { @@ -12648,12 +12664,16 @@ } }, { - "name": "lastSlot", + "name": "targetBase", + "type": "i64" + }, + { + "name": "lastOracleSlot", "type": "u64" }, { - "name": "targetBase", - "type": "i64" + "name": "lastPositionSlot", + "type": "u64" } ] } @@ -15020,7 +15040,7 @@ "name": "UseMMOraclePrice" }, { - "name": "UpdateLpConstituentTargetBase" + "name": "UpdateAmmCache" }, { "name": "UpdateLpPoolAum" @@ -19720,8 +19740,5 @@ "name": "Unauthorized", "msg": "Unauthorized for operation" } - ], - "metadata": { - "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" - } + ] } \ No newline at end of file diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 4b5b1f8798..c3e7c04abb 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1738,7 +1738,8 @@ export type AmmConstituentMapping = { export type TargetDatum = { costToTradeBps: number; - lastSlot: BN; + lastOracleSlot: BN; + lastPositionSlot: BN; targetBase: BN; }; From 89d788049f1f71e8fd10f28c3079331e579cd589 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 14 Oct 2025 18:56:50 -0400 Subject: [PATCH 119/159] add signer to deposit record --- programs/drift/src/controller/isolated_position.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs index 5bf940354b..45b0895435 100644 --- a/programs/drift/src/controller/isolated_position.rs +++ b/programs/drift/src/controller/isolated_position.rs @@ -145,6 +145,7 @@ pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( market_index: spot_market_index, explanation: DepositExplanation::None, transfer_user: None, + signer: None, }; emit!(deposit_record); @@ -409,6 +410,7 @@ pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( total_withdraws_after: user.total_withdraws, explanation: DepositExplanation::None, transfer_user: None, + signer: None, }; emit!(deposit_record); From 0ca25a066f23a06bb09f021fba9c17ab82c91d6b Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Thu, 16 Oct 2025 09:57:26 -0700 Subject: [PATCH 120/159] Moose review (#1948) * better cap fees * add more constraints for user token accounts * use oracle map for updating aum * cargo tests pass and adding oracle map usage to derivative constituent in aum target as well --- programs/drift/src/instructions/lp_admin.rs | 1 - programs/drift/src/instructions/lp_pool.rs | 30 +- programs/drift/src/state/lp_pool.rs | 108 +++++- programs/drift/src/state/lp_pool/tests.rs | 369 ++++++++++++++++++-- sdk/src/driftClient.ts | 92 ++--- 5 files changed, 481 insertions(+), 119 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 5c406501fe..2d53cc71e2 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1079,7 +1079,6 @@ pub struct InitializeLpPool<'info> { spot_market_index: u16, )] pub struct InitializeConstituent<'info> { - #[account()] pub state: Box>, #[account( mut, diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 42a0828037..5699598636 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -3,7 +3,6 @@ use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::ids::lp_pool_swap_wallet; use crate::math::constants::PRICE_PRECISION_I64; -use crate::math::oracle::OracleValidity; use crate::state::events::{DepositDirection, LPBorrowLendDepositRecord}; use crate::state::paused_operations::ConstituentLpOperation; use crate::validation::whitelist::validate_whitelist_token; @@ -32,7 +31,6 @@ use crate::{ update_constituent_target_base_for_derivatives, AmmConstituentDatum, AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, ConstituentTargetBaseFixed, LPPool, TargetsDatum, LP_POOL_SWAP_AUM_UPDATE_DELAY, - MAX_ORACLE_STALENESS_FOR_TARGET_CALC, MAX_STALENESS_FOR_TARGET_CALC, }, oracle_map::OracleMap, perp_market_map::MarketSet, @@ -45,7 +43,6 @@ use crate::{ }, validate, }; -use std::convert::TryFrom; use std::iter::Peekable; use std::slice::Iter; @@ -164,7 +161,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( let AccountMaps { perp_market_map: _, spot_market_map, - oracle_map: _, + mut oracle_map, } = load_maps( remaining_accounts, &MarketSet::new(), @@ -202,6 +199,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( slot, &constituent_map, &spot_market_map, + &mut oracle_map, &constituent_target_base, &amm_cache, )?; @@ -227,6 +225,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, )?; @@ -1316,8 +1315,6 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( )?; let out_oracle = out_oracle.clone(); - // TODO: check self.aum validity - if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { msg!( "Out oracle data for spot market {} is invalid for lp pool swap.", @@ -1330,7 +1327,7 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( out_constituent.constituent_index, &out_spot_market, out_oracle.price, - lp_pool.last_aum, // TODO: remove out_amount * out_oracle to est post remove_liquidity aum + lp_pool.last_aum, )?; let dlp_total_supply = ctx.accounts.lp_mint.supply; @@ -1410,7 +1407,6 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( let deposit_plus_token_amount_before = amount.safe_add(spot_market_vault.amount)?; let oracle_data = oracle_map.get_price_data(&oracle_id)?; - let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; controller::spot_balance::update_spot_market_cumulative_interest( &mut spot_market, @@ -1425,10 +1421,6 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( .cast::()? .safe_sub(constituent.last_spot_balance_token_amount)?; - if constituent.last_oracle_slot < oracle_data_slot { - constituent.last_oracle_price = oracle_data.price; - constituent.last_oracle_slot = oracle_data_slot; - } constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); let balance_before = constituent.get_full_token_amount(&spot_market)?; @@ -1559,11 +1551,6 @@ pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( .cast::()? .safe_sub(constituent.last_spot_balance_token_amount)?; - if constituent.last_oracle_slot < oracle_data_slot { - constituent.last_oracle_price = oracle_data.price; - constituent.last_oracle_slot = oracle_data_slot; - } - let mint = &Some(*ctx.accounts.mint.clone()); transfer_from_program_vault( amount, @@ -1865,12 +1852,12 @@ pub struct LPPoolSwap<'info> { #[account( mut, - constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() )] pub user_in_token_account: Box>, #[account( mut, - constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() )] pub user_out_token_account: Box>, @@ -1900,7 +1887,6 @@ pub struct LPPoolSwap<'info> { pub authority: Signer<'info>, - // TODO: in/out token program pub token_program: Interface<'info, TokenInterface>, } @@ -1974,7 +1960,7 @@ pub struct LPPoolAddLiquidity<'info> { #[account( mut, - constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() )] pub user_in_token_account: Box>, @@ -2058,7 +2044,7 @@ pub struct LPPoolRemoveLiquidity<'info> { #[account( mut, - constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() )] pub user_out_token_account: Box>, #[account( diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 667e8da54d..033d67c780 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::f32::consts::E; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; @@ -6,13 +7,16 @@ use crate::math::constants::{ BASE_PRECISION_I128, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, }; +use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::math::spot_balance::{get_signed_token_amount, get_token_amount}; use crate::state::amm_cache::{AmmCacheFixed, CacheInfo}; use crate::state::constituent_map::ConstituentMap; +use crate::state::oracle_map::OracleMap; use crate::state::paused_operations::ConstituentLpOperation; use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::MarketType; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use enumflags2::BitFlags; @@ -32,9 +36,8 @@ pub const CONSTITUENT_CORRELATIONS_PDA_SEED: &str = "constituent_correlations"; pub const CONSTITUENT_VAULT_PDA_SEED: &str = "CONSTITUENT_VAULT"; pub const LP_POOL_TOKEN_VAULT_PDA_SEED: &str = "LP_POOL_TOKEN_VAULT"; -pub const BASE_SWAP_FEE: i128 = 300; // 0.75% in PERCENTAGE_PRECISION -pub const MAX_SWAP_FEE: i128 = 75_000; // 0.75% in PERCENTAGE_PRECISION -pub const MIN_SWAP_FEE: i128 = 200; // 0.75% in PERCENTAGE_PRECISION +pub const BASE_SWAP_FEE: i128 = 300; // 0.3% in PERCENTAGE_PRECISION +pub const MAX_SWAP_FEE: i128 = 37_500; // 37.5% in PERCENTAGE_PRECISION pub const MIN_AUM_EXECUTION_FEE: u128 = 10_000_000_000_000; @@ -225,6 +228,9 @@ impl LPPool { out_target_oracle_slot_delay, )?; + in_fee = in_fee.min(MAX_SWAP_FEE); + out_fee = out_fee.min(MAX_SWAP_FEE); + let in_fee_amount = in_amount .cast::()? .safe_mul(in_fee)? @@ -279,6 +285,7 @@ impl LPPool { in_target_position_slot_delay, in_target_oracle_slot_delay, )?; + in_fee_pct = in_fee_pct.min(MAX_SWAP_FEE * 2); let in_fee_amount = in_amount .cast::()? @@ -383,6 +390,8 @@ impl LPPool { out_target_oracle_slot_delay, )?; out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + out_fee_pct = out_fee_pct.min(MAX_SWAP_FEE * 2); + let out_fee_amount = out_amount .cast::()? .safe_mul(out_fee_pct)? @@ -629,10 +638,7 @@ impl LPPool { .safe_add(out_quadratic_inventory_fee)? .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; - Ok(( - total_in_fee.min(MAX_SWAP_FEE.safe_div(2)?), - total_out_fee.min(MAX_SWAP_FEE.safe_div(2)?), - )) + Ok((total_in_fee, total_out_fee)) } pub fn get_target_uncertainty_fees( @@ -686,6 +692,7 @@ impl LPPool { slot: u64, constituent_map: &ConstituentMap, spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, constituent_target_base: &AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, amm_cache: &AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed>, ) -> DriftResult<(u128, i128, BTreeMap>)> { @@ -723,10 +730,28 @@ impl LPPool { } let spot_market = spot_market_map.get_ref(&constituent.spot_market_index)?; + let oracle_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &spot_market.oracle_id(), + spot_market.historical_oracle_data.last_oracle_price_twap, + spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + if !is_oracle_valid_for_action( + oracle_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is not valid for action", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle.into()); + } let constituent_aum = constituent .get_full_token_amount(&spot_market)? - .safe_mul(constituent.last_oracle_price as i128)? + .safe_mul(oracle_and_validity.0.price as i128)? .safe_div(10_i128.pow(spot_market.decimals))?; msg!( "constituent: {}, balance: {}, aum: {}, deriv index: {}, bl token balance {}, bl balance type {}, vault balance: {}", @@ -747,7 +772,7 @@ impl LPPool { .get(constituent.constituent_index as u32) .target_base .cast::()? - .safe_mul(constituent.last_oracle_price.cast::()?)? + .safe_mul(oracle_and_validity.0.price.cast::()?)? .safe_div(10_i128.pow(constituent.decimals as u32))? .cast::()?; crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?; @@ -1579,8 +1604,8 @@ impl ConstituentCorrelations { "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" )?; - self.correlations[(i as usize * num_constituents + j as usize)] = corr; - self.correlations[(j as usize * num_constituents + i as usize)] = corr; + self.correlations[i as usize * num_constituents + j as usize] = corr; + self.correlations[j as usize * num_constituents + i as usize] = corr; self.validate()?; @@ -1662,34 +1687,81 @@ pub fn update_constituent_target_base_for_derivatives( derivative_groups: &BTreeMap>, constituent_map: &ConstituentMap, spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, constituent_target_base: &mut AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, ) -> DriftResult<()> { for (parent_index, constituent_indexes) in derivative_groups.iter() { let parent_constituent = constituent_map.get_ref(parent_index)?; + + let parent_spot_market = spot_market_map.get_ref(&parent_constituent.spot_market_index)?; + let parent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + parent_spot_market.market_index, + &parent_spot_market.oracle_id(), + parent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + parent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + if !is_oracle_valid_for_action( + parent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Parent constituent {} oracle is invalid", + parent_constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + let parent_constituent_price = parent_oracle_price_and_validity.0.price; + let parent_target_base = constituent_target_base .get(*parent_index as u32) .target_base; let target_parent_weight = calculate_target_weight( parent_target_base, &*spot_market_map.get_ref(&parent_constituent.spot_market_index)?, - parent_constituent.last_oracle_price, + parent_oracle_price_and_validity.0.price, aum, )?; let mut derivative_weights_sum: u64 = 0; for constituent_index in constituent_indexes { let constituent = constituent_map.get_ref(constituent_index)?; - if constituent.last_oracle_price - < parent_constituent - .last_oracle_price + let constituent_spot_market = + spot_market_map.get_ref(&constituent.spot_market_index)?; + let constituent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &constituent_spot_market.oracle_id(), + constituent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + constituent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + if !is_oracle_valid_for_action( + constituent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is invalid", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + + if constituent_oracle_price_and_validity.0.price + < parent_constituent_price .safe_mul(constituent.constituent_derivative_depeg_threshold as i64)? .safe_div(PERCENTAGE_PRECISION_I64)? { msg!( "Constituent {} last oracle price {} is too low compared to parent constituent {} last oracle price {}. Assuming depegging and setting target base to 0.", constituent.constituent_index, - constituent.last_oracle_price, + constituent_oracle_price_and_validity.0.price, parent_constituent.constituent_index, - parent_constituent.last_oracle_price + parent_constituent_price ); constituent_target_base .get_mut(*constituent_index as u32) @@ -1714,7 +1786,7 @@ pub fn update_constituent_target_base_for_derivatives( .safe_mul(target_weight)? .safe_div(PERCENTAGE_PRECISION_I128)? .safe_mul(10_i128.pow(constituent.decimals as u32))? - .safe_div(constituent.last_oracle_price as i128)?; + .safe_div(constituent_oracle_price_and_validity.0.price as i128)?; msg!( "constituent: {}, target base: {}", diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index a460bcd151..19c15b45be 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -273,7 +273,6 @@ mod tests { price, }]; let aum = 1_000_000; - let now_ts = 1234; let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 1, @@ -384,7 +383,6 @@ mod tests { ]; let aum = 1_000_000; - let now_ts = 999; let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: amm_mapping_data.len() as u32, @@ -469,8 +467,6 @@ mod tests { price: 142_000_000, }]; - let now_ts = 111; - let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 1, ..ConstituentTargetBaseFixed::default() @@ -499,8 +495,6 @@ mod tests { #[cfg(test)] mod swap_tests { - use core::slice; - use crate::math::constants::{ PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I128, PRICE_PRECISION_I64, SPOT_BALANCE_PRECISION, @@ -774,7 +768,7 @@ mod swap_tests { in_target_position_delay: u64, in_target_oracle_delay: u64, last_aum: u128, - now: i64, + _now: i64, in_decimals: u32, in_amount: u128, dlp_total_supply: u64, @@ -964,9 +958,9 @@ mod swap_tests { fn get_remove_liquidity_mint_amount_scenario( out_target_position_delay: u64, - out_target_oracle_delay: u64, + _out_target_oracle_delay: u64, last_aum: u128, - now: i64, + _now: i64, in_decimals: u32, lp_burn_amount: u64, dlp_total_supply: u64, @@ -2378,6 +2372,8 @@ mod update_aum_tests { state::lp_pool::*, state::oracle::HistoricalOracleData, state::oracle::OracleSource, + state::oracle_map::OracleMap, + state::pyth_lazer_oracle::PythLazerOracle, state::spot_market::SpotMarket, state::spot_market_map::SpotMarketMap, state::zero_copy::AccountZeroCopyMut, @@ -2462,6 +2458,56 @@ mod update_aum_tests { ) .unwrap(); + // Create simple PythLazer oracle accounts for non-quote assets with prices matching constituents + // Use exponent -6 so values are already in PRICE_PRECISION units + let sol_oracle_pubkey = Pubkey::new_unique(); + let mut sol_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + sol_oracle, + &sol_oracle_pubkey, + PythLazerOracle, + sol_oracle_account_info + ); + + let btc_oracle_pubkey = Pubkey::new_unique(); + let mut btc_oracle = PythLazerOracle { + price: 100_000 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + btc_oracle, + &btc_oracle_pubkey, + PythLazerOracle, + btc_oracle_account_info + ); + + let bonk_oracle_pubkey = Pubkey::new_unique(); + let mut bonk_oracle = PythLazerOracle { + price: 22, // $0.000022 in PRICE_PRECISION + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + bonk_oracle, + &bonk_oracle_pubkey, + PythLazerOracle, + bonk_oracle_account_info + ); + // Create spot markets let mut usdc_spot_market = SpotMarket { market_index: 0, @@ -2475,30 +2521,35 @@ mod update_aum_tests { let mut sol_spot_market = SpotMarket { market_index: 1, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: sol_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(200 * PRICE_PRECISION_I64), ..SpotMarket::default() }; create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); let mut btc_spot_market = SpotMarket { market_index: 2, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: btc_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 8, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price( + 100_000 * PRICE_PRECISION_I64, + ), ..SpotMarket::default() }; create_anchor_account_info!(btc_spot_market, SpotMarket, btc_spot_market_account_info); let mut bonk_spot_market = SpotMarket { market_index: 3, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: bonk_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 5, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(22), ..SpotMarket::default() }; create_anchor_account_info!(bonk_spot_market, SpotMarket, bonk_spot_market_account_info); @@ -2511,6 +2562,21 @@ mod update_aum_tests { ]; let spot_market_map = SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + // Build an oracle map containing the three non-quote oracles + let oracle_accounts = vec![ + sol_oracle_account_info.clone(), + btc_oracle_account_info.clone(), + bonk_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + msg!( + "oracle map entry 0 {:?}", + oracle_map + .get_price_data(&sol_spot_market.oracle_id()) + .unwrap() + ); // Create constituent target base let target_fixed = RefCell::new(ConstituentTargetBaseFixed { @@ -2518,7 +2584,7 @@ mod update_aum_tests { ..ConstituentTargetBaseFixed::default() }); let target_data = RefCell::new([0u8; 128]); // 4 * 32 bytes per TargetsDatum - let mut constituent_target_base = + let constituent_target_base = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), data: target_data.borrow_mut(), @@ -2541,6 +2607,7 @@ mod update_aum_tests { 101, // slot &constituent_map, &spot_market_map, + &mut oracle_map, &constituent_target_base, &amm_cache, ); @@ -2677,6 +2744,8 @@ mod update_constituent_target_base_for_derivatives_tests { use crate::state::constituent_map::ConstituentMap; use crate::state::lp_pool::{Constituent, ConstituentTargetBaseFixed, TargetsDatum}; use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::pyth_lazer_oracle::PythLazerOracle; use crate::state::spot_market::SpotMarket; use crate::state::spot_market_map::SpotMarketMap; use crate::state::zero_copy::AccountZeroCopyMut; @@ -2781,13 +2850,79 @@ mod update_constituent_target_base_for_derivatives_tests { ]; let constituent_map = ConstituentMap::load_multiple(constituents_list, true).unwrap(); - // Create spot markets + // Create oracles for parent and derivatives, with prices matching their last_oracle_price + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 205 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + + let derivative3_oracle_pubkey = Pubkey::new_unique(); + let mut derivative3_oracle = PythLazerOracle { + price: 210 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative3_oracle, + &derivative3_oracle_pubkey, + PythLazerOracle, + derivative3_oracle_account_info + ); + + // Create spot markets bound to the above oracles let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2798,10 +2933,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative1_spot_market = SpotMarket { market_index: derivative1_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2812,10 +2948,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative2_spot_market = SpotMarket { market_index: derivative2_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2826,10 +2963,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative3_spot_market = SpotMarket { market_index: derivative3_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative3_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(derivative3_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2846,6 +2984,16 @@ mod update_constituent_target_base_for_derivatives_tests { ]; let spot_market_map = SpotMarketMap::load_multiple(spot_market_list, true).unwrap(); + // Build an oracle map for parent and derivatives + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + derivative3_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + // Create constituent target base let num_constituents = 4; // Fixed: parent + 3 derivatives let target_fixed = RefCell::new(ConstituentTargetBaseFixed { @@ -2913,6 +3061,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); @@ -3049,12 +3198,47 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Create PythLazer oracles corresponding to prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + // Create spot markets let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3065,9 +3249,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative_spot_market = SpotMarket { market_index: derivative_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3085,6 +3271,14 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + // Create constituent target base let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 2, @@ -3116,6 +3310,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); @@ -3292,11 +3487,47 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Create PythLazer oracles so update_constituent_target_base_for_derivatives can fetch current prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + + // Spot markets bound to the test oracles let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3307,9 +3538,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative_spot_market = SpotMarket { market_index: derivative_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3327,6 +3560,14 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 2, ..ConstituentTargetBaseFixed::default() @@ -3355,6 +3596,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); @@ -3446,11 +3688,60 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Oracles + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 198 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3461,9 +3752,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative1_spot_market = SpotMarket { market_index: derivative1_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3474,9 +3767,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative2_spot_market = SpotMarket { market_index: derivative2_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3495,6 +3790,15 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 3, ..ConstituentTargetBaseFixed::default() @@ -3525,6 +3829,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 37ae130359..f3e023ad48 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -362,8 +362,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') { @@ -848,8 +848,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) { @@ -1001,7 +1001,7 @@ export class DriftClient { [...this.authoritySubAccountMap.values()][0][0] ?? 0, new PublicKey( [...this.authoritySubAccountMap.keys()][0] ?? - this.authority.toString() + this.authority.toString() ) ); } @@ -3311,19 +3311,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 ( @@ -4363,14 +4363,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; @@ -4518,10 +4518,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({ @@ -5158,14 +5158,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) { @@ -5381,14 +5381,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) { @@ -6727,8 +6727,8 @@ export class DriftClient { makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo - ? [makerInfo] - : []; + ? [makerInfo] + : []; const userAccounts = [this.getUserAccount(subAccountId)]; for (const maker of makerInfo) { @@ -6974,13 +6974,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; } @@ -10699,8 +10699,8 @@ export class DriftClient { ): Promise { const remainingAccounts = userAccount ? this.getRemainingAccounts({ - userAccounts: [userAccount], - }) + userAccounts: [userAccount], + }) : undefined; const ix = await this.program.instruction.disableUserHighLeverageMode( From b29503185fa8963da23ddb022f520ebe62273201 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:56:01 -0700 Subject: [PATCH 121/159] remove unnecessary admin func --- programs/drift/src/instructions/lp_admin.rs | 25 +++++++++---------- programs/drift/src/lib.rs | 7 ------ sdk/src/adminClient.ts | 27 +++------------------ 3 files changed, 14 insertions(+), 45 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 2d53cc71e2..bc5910dae8 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -113,20 +113,6 @@ pub fn handle_initialize_lp_pool( Ok(()) } -pub fn handle_increase_lp_pool_max_aum( - ctx: Context, - new_max_aum: u128, -) -> Result<()> { - let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; - msg!( - "lp pool max aum: {:?} -> {:?}", - lp_pool.max_aum, - new_max_aum - ); - lp_pool.max_aum = new_max_aum; - Ok(()) -} - pub fn handle_initialize_constituent<'info>( ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, spot_market_index: u16, @@ -399,6 +385,7 @@ pub struct LpPoolParams { pub volatility: Option, pub gamma_execution: Option, pub xi: Option, + pub max_aum: Option, pub whitelist_mint: Option, } @@ -445,6 +432,16 @@ pub fn handle_update_lp_pool_params<'info>( lp_pool.whitelist_mint = whitelist_mint; } + if let Some(max_aum) = lp_pool_params.max_aum { + validate!( + max_aum >= lp_pool.max_aum, + ErrorCode::DefaultError, + "new max_aum must be greater than or equal to current max_aum" + )?; + msg!("max_aum: {:?} -> {:?}", lp_pool.max_aum, max_aum); + lp_pool.max_aum = max_aum; + } + Ok(()) } diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 23dbb42719..2299adb213 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1809,13 +1809,6 @@ pub mod drift { ) } - pub fn increase_lp_pool_max_aum( - ctx: Context, - new_max_aum: u128, - ) -> Result<()> { - handle_increase_lp_pool_max_aum(ctx, new_max_aum) - } - pub fn update_high_leverage_mode_config( ctx: Context, max_users: u32, diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index fcfd6b1a7e..835f8823ea 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -5323,30 +5323,6 @@ export class AdminClient extends DriftClient { ]; } - public async increaseLpPoolMaxAum( - name: string, - newMaxAum: BN - ): Promise { - const ixs = await this.getIncreaseLpPoolMaxAumIx(name, newMaxAum); - const tx = await this.buildTransaction(ixs); - const { txSig } = await this.sendTransaction(tx, []); - return txSig; - } - - public async getIncreaseLpPoolMaxAumIx( - name: string, - newMaxAum: BN - ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, encodeName(name)); - return this.program.instruction.increaseLpPoolMaxAum(newMaxAum, { - accounts: { - admin: this.wallet.publicKey, - lpPool, - state: await this.getStatePublicKey(), - }, - }); - } - public async initializeConstituent( lpPoolName: number[], initializeConstituentParams: InitializeConstituentParams @@ -5597,6 +5573,7 @@ export class AdminClient extends DriftClient { gammaExecution?: number; xi?: number; whitelistMint?: PublicKey; + maxAum?: BN; } ): Promise { const ixs = await this.getUpdateLpPoolParamsIx( @@ -5616,6 +5593,7 @@ export class AdminClient extends DriftClient { gammaExecution?: number; xi?: number; whitelistMint?: PublicKey; + maxAum?: BN; } ): Promise { const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); @@ -5628,6 +5606,7 @@ export class AdminClient extends DriftClient { gammaExecution: null, xi: null, whitelistMint: null, + maxAum: null, }, updateLpPoolParams ), From 453d93f391dda383fee567ffa3e333d3c68beb5c Mon Sep 17 00:00:00 2001 From: lil perp Date: Thu, 16 Oct 2025 19:00:29 -0400 Subject: [PATCH 122/159] program: add transfer isolated pos deposit into swift (#1964) * program: add transfer isolated pos deposit into swift * cargo test --- .../drift/src/controller/isolated_position.rs | 16 +++ programs/drift/src/instructions/keeper.rs | 19 ++++ programs/drift/src/state/order_params.rs | 2 + programs/drift/src/state/perp_market.rs | 2 +- .../drift/src/validation/sig_verification.rs | 3 + .../src/validation/sig_verification/tests.rs | 103 ++++++++++++++++++ 6 files changed, 144 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs index 5bf940354b..dac6d74b35 100644 --- a/programs/drift/src/controller/isolated_position.rs +++ b/programs/drift/src/controller/isolated_position.rs @@ -8,6 +8,7 @@ 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; @@ -172,6 +173,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( 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)?; @@ -198,6 +200,8 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( Some(oracle_price_data), now, )?; + + tvl_before = spot_market.get_tvl()?; } if amount > 0 { @@ -299,6 +303,18 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( 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(()) } diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 25d2bb7426..cc8c690249 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -12,6 +12,7 @@ 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, }; @@ -630,11 +631,13 @@ 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()?; 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(), @@ -651,6 +654,7 @@ 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>, @@ -764,6 +768,21 @@ 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 { + transfer_isolated_perp_position_deposit( + taker, + 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 = diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 69e1c6710d..5e2b0fd996 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -865,6 +865,7 @@ pub struct SignedMsgOrderParamsMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -876,6 +877,7 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index c373794967..18b0e81309 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -10,7 +10,7 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm; use crate::math::casting::Cast; #[cfg(test)] -use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; +use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64}; use crate::math::constants::{ AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 66ba1cab98..a7f0954b8c 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -58,6 +58,7 @@ pub struct VerifiedMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub isolated_position_deposit: Option, pub signature: [u8; 64], } @@ -96,6 +97,7 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + isolated_position_deposit: deserialized.isolated_position_deposit, signature: *signature, }); } else { @@ -123,6 +125,7 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + isolated_position_deposit: deserialized.isolated_position_deposit, signature: *signature, }); } diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs index fae2456b43..ec7cce44fb 100644 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -141,6 +141,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, 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]; @@ -290,4 +339,58 @@ mod sig_verification { assert_eq!(order_params.auction_start_price, Some(240000000i64)); assert_eq!(order_params.auction_end_price, Some(238000000i64)); } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_isolated_position_deposit() { + let signature = [1u8; 64]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, + 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, + 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, + 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, + 59, 0, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + 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, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) + ); + 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, 1000000000u64); + assert_eq!(tp.trigger_price, 230000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 1000000000u64); + assert_eq!(sl.trigger_price, 250000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + 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(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } } From 82fc87f13b57bce63ba16a308df78172f8bb4cf5 Mon Sep 17 00:00:00 2001 From: lil perp Date: Thu, 16 Oct 2025 19:01:15 -0400 Subject: [PATCH 123/159] program: auto transfer to cross margin account (#1939) * program: start auto transfer * update * cargo fmt -- --- .../drift/src/controller/isolated_position.rs | 101 +++++++++++------- .../src/controller/isolated_position/tests.rs | 12 +-- programs/drift/src/controller/pnl.rs | 54 +++++++--- programs/drift/src/controller/pnl/tests.rs | 2 +- programs/drift/src/instructions/keeper.rs | 6 +- .../src/instructions/optional_accounts.rs | 4 +- programs/drift/src/instructions/user.rs | 2 +- programs/drift/src/state/perp_market.rs | 4 +- programs/drift/src/state/user.rs | 7 ++ sdk/src/idl/drift.json | 3 + tests/isolatedPositionDriftClient.ts | 11 +- 11 files changed, 137 insertions(+), 69 deletions(-) diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs index dac6d74b35..8f99058c7b 100644 --- a/programs/drift/src/controller/isolated_position.rs +++ b/programs/drift/src/controller/isolated_position.rs @@ -20,6 +20,8 @@ use crate::state::user::{User, UserStats}; use crate::validate; use anchor_lang::prelude::*; +use super::position::get_position_index; + #[cfg(test)] mod tests; @@ -155,7 +157,7 @@ pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( user: &mut User, - user_stats: &mut UserStats, + user_stats: Option<&mut UserStats>, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -227,25 +229,30 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( drop(spot_market); - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( - &perp_market_map, - &spot_market_map, - oracle_map, - MarginRequirementType::Initial, - spot_market_index, - amount as u128, - user_stats, - now, - )?; - - validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, oracle_map)?; - - if user.is_cross_margin_being_liquidated() { - user.exit_cross_margin_liquidation(); - } - - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; + 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)?; @@ -254,8 +261,15 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( .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.unsigned_abs() as u128 <= isolated_perp_position_token_amount, + amount <= isolated_perp_position_token_amount, ErrorCode::InsufficientCollateral, "user has insufficient deposit for market {}", spot_market_index @@ -263,7 +277,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; update_spot_balances_and_cumulative_deposits( - amount.abs() as u128, + amount, &SpotBalanceType::Deposit, &mut spot_market, &mut user.spot_positions[spot_position_index], @@ -272,7 +286,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( )?; update_spot_balances( - amount.abs() as u128, + amount, &SpotBalanceType::Borrow, &mut spot_market, user.force_get_isolated_perp_position_mut(perp_market_index)?, @@ -281,23 +295,30 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( drop(spot_market); - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( - &perp_market_map, - &spot_market_map, - oracle_map, - MarginRequirementType::Initial, - 0, - 0, - user_stats, - now, - )?; - - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } - - if user.is_cross_margin_being_liquidated() { - user.exit_cross_margin_liquidation(); + 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); + } } } diff --git a/programs/drift/src/controller/isolated_position/tests.rs b/programs/drift/src/controller/isolated_position/tests.rs index 2c2cac1660..2a57899a3c 100644 --- a/programs/drift/src/controller/isolated_position/tests.rs +++ b/programs/drift/src/controller/isolated_position/tests.rs @@ -311,7 +311,7 @@ pub mod transfer_isolated_perp_position_deposit { transfer_isolated_perp_position_deposit( &mut user, - &mut user_stats, + Some(&mut user_stats), &perp_market_map, &spot_market_map, &mut oracle_map, @@ -416,7 +416,7 @@ pub mod transfer_isolated_perp_position_deposit { let result = transfer_isolated_perp_position_deposit( &mut user, - &mut user_stats, + Some(&mut user_stats), &perp_market_map, &spot_market_map, &mut oracle_map, @@ -507,7 +507,7 @@ pub mod transfer_isolated_perp_position_deposit { let result = transfer_isolated_perp_position_deposit( &mut user, - &mut user_stats, + Some(&mut user_stats), &perp_market_map, &spot_market_map, &mut oracle_map, @@ -599,7 +599,7 @@ pub mod transfer_isolated_perp_position_deposit { transfer_isolated_perp_position_deposit( &mut user, - &mut user_stats, + Some(&mut user_stats), &perp_market_map, &spot_market_map, &mut oracle_map, @@ -700,7 +700,7 @@ pub mod transfer_isolated_perp_position_deposit { let result = transfer_isolated_perp_position_deposit( &mut user, - &mut user_stats, + Some(&mut user_stats), &perp_market_map, &spot_market_map, &mut oracle_map, @@ -793,7 +793,7 @@ pub mod transfer_isolated_perp_position_deposit { let result = transfer_isolated_perp_position_deposit( &mut user, - &mut user_stats, + Some(&mut user_stats), &perp_market_map, &spot_market_map, &mut oracle_map, diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index dd199eba9a..d1640c1251 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -1,5 +1,6 @@ use crate::controller::amm::{update_pnl_pool_and_user_balance, update_pool_balances}; use crate::controller::funding::settle_funding_payment; +use crate::controller::isolated_position::transfer_isolated_perp_position_deposit; use crate::controller::orders::{cancel_orders, validate_market_within_price_band}; use crate::controller::position::{ get_position_index, update_position_and_market, update_quote_asset_amount, @@ -21,6 +22,7 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; use crate::msg; + use crate::state::events::{OrderActionExplanation, SettlePnlExplanation, SettlePnlRecord}; use crate::state::oracle_map::OracleMap; use crate::state::paused_operations::PerpOperation; @@ -30,7 +32,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, User}; +use crate::state::user::{MarketType, User, UserStats}; use crate::validate; use anchor_lang::prelude::Pubkey; use anchor_lang::prelude::*; @@ -56,6 +58,7 @@ pub fn settle_pnl( mut mode: SettlePnlMode, ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + let slot = clock.slot; let now = clock.unix_timestamp; let tvl_before; let deposits_balance_before; @@ -109,8 +112,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)?; @@ -207,13 +210,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)?; @@ -237,16 +240,16 @@ pub fn settle_pnl( let user_quote_token_amount = if is_isolated_position { user.perp_positions[position_index] - .get_isolated_token_amount(spot_market)? + .get_isolated_token_amount(&spot_market)? .cast()? } else { user.get_quote_spot_position() - .get_signed_token_amount(spot_market)? + .get_signed_token_amount(&spot_market)? }; let pnl_to_settle_with_user = update_pool_balances( - perp_market, - spot_market, + &mut perp_market, + &mut spot_market, user_quote_token_amount, user_unsettled_pnl, now, @@ -292,7 +295,7 @@ pub fn settle_pnl( 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)?; + let token_amount = perp_position.get_isolated_token_amount(&spot_market)?; validate!( token_amount >= pnl_to_settle_with_user.unsigned_abs(), @@ -309,7 +312,7 @@ pub fn settle_pnl( } else { &SpotBalanceType::Borrow }, - spot_market, + &mut spot_market, perp_position, false, )?; @@ -321,7 +324,7 @@ pub fn settle_pnl( } else { &SpotBalanceType::Borrow }, - spot_market, + &mut spot_market, user.get_quote_spot_position_mut(), false, )?; @@ -329,7 +332,7 @@ pub fn settle_pnl( update_quote_asset_amount( &mut user.perp_positions[position_index], - perp_market, + &mut perp_market, -pnl_to_settle_with_user.cast()?, )?; @@ -338,10 +341,31 @@ 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); + + if user.perp_positions[position_index].can_transfer_isolated_position_deposit() { + transfer_isolated_perp_position_deposit( + user, + None, + perp_market_map, + spot_market_map, + oracle_map, + slot, + now, + 0, + market_index, + i64::MIN, + )?; + } + + 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 { diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index e2f10ac990..da56517ffa 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -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()); } diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index cc8c690249..297351ba42 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -922,12 +922,13 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( "user have pool_id 0" )?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set(market_index), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, @@ -998,12 +999,13 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( let user_key = ctx.accounts.user.key(); 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, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set_from_vec(&market_indexes), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index 7abe5c33d2..28f69e2a4b 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -5,7 +5,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; @@ -15,9 +14,10 @@ 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}; +use anchor_lang::prelude::{AccountInfo, Interface, Pubkey}; use anchor_lang::prelude::{AccountLoader, InterfaceAccount}; use anchor_lang::Discriminator; use anchor_spl::token::TokenAccount; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 23133b8e87..4bba2f8a4a 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2019,7 +2019,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( controller::isolated_position::transfer_isolated_perp_position_deposit( user, - user_stats, + Some(user_stats), &perp_market_map, &spot_market_map, &mut oracle_map, diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 18b0e81309..267f571cc4 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -10,7 +10,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm; use crate::math::casting::Cast; #[cfg(test)] -use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64}; +use crate::math::constants::{ + AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64, +}; use crate::math::constants::{ AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 6071635e16..4a117c1a97 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1281,6 +1281,13 @@ impl PerpPosition { 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.is_open_position() + && !self.has_open_order() + && !self.has_unsettled_pnl() + } } impl SpotBalance for PerpPosition { diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index e621bcb8df..8455798584 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -12534,6 +12534,9 @@ }, { "name": "UpdateHighLeverageMode" + }, + { + "name": "EnableAutoTransfer" } ] } diff --git a/tests/isolatedPositionDriftClient.ts b/tests/isolatedPositionDriftClient.ts index 644ae91308..fb91494969 100644 --- a/tests/isolatedPositionDriftClient.ts +++ b/tests/isolatedPositionDriftClient.ts @@ -478,7 +478,10 @@ describe('drift client', () => { assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(0))); console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); assert.ok( - driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(9855998)) + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(0)) + ); + assert.ok( + driftClient.getQuoteAssetTokenAmount().eq(new BN(9855998)) ); console.log( driftClient @@ -514,6 +517,12 @@ describe('drift client', () => { }); 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); From 81561dcf12cb76eb10db504373ee9b41495b2832 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:05:04 -0700 Subject: [PATCH 124/159] merge dlp and push --- sdk/src/idl/drift.json | 4497 ++++++++++++++++++++++++++++++++++------ sdk/src/types.ts | 3 +- 2 files changed, 3832 insertions(+), 668 deletions(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 0095db2d91..52e1f4ceed 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": [ @@ -4285,6 +4442,11 @@ "isMut": true, "isSigner": false }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, { "name": "oracle", "isMut": false, @@ -4413,6 +4575,58 @@ } ] }, + { + "name": "initializeAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateInitialAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializePredictionMarket", "accounts": [ @@ -4627,21 +4841,16 @@ ] }, { - "name": "settleExpiredMarketPoolsToRevenuePool", + "name": "updatePerpMarketLpPoolPausedOperations", "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, { "name": "admin", "isMut": false, "isSigner": true }, { - "name": "spotMarket", - "isMut": true, + "name": "state", + "isMut": false, "isSigner": false }, { @@ -4650,66 +4859,162 @@ "isSigner": false } ], - "args": [] + "args": [ + { + "name": "lpPausedOperations", + "type": "u8" + } + ] }, { - "name": "depositIntoPerpMarketFeePool", + "name": "updatePerpMarketLpPoolStatus", "accounts": [ - { - "name": "state", - "isMut": true, - "isSigner": false - }, - { - "name": "perpMarket", - "isMut": true, - "isSigner": false - }, { "name": "admin", "isMut": false, "isSigner": true }, { - "name": "sourceVault", - "isMut": true, - "isSigner": false - }, - { - "name": "driftSigner", + "name": "state", "isMut": false, "isSigner": false }, { - "name": "quoteSpotMarket", + "name": "perpMarket", "isMut": true, "isSigner": false }, { - "name": "spotMarketVault", + "name": "ammCache", "isMut": true, "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false } ], "args": [ { - "name": "amount", - "type": "u64" + "name": "lpStatus", + "type": "u8" } ] }, { - "name": "updatePerpMarketPnlPool", + "name": "updatePerpMarketLpPoolFeeTransferScalar", "accounts": [ { - "name": "state", + "name": "admin", "isMut": false, - "isSigner": false + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "optionalLpFeeTransferScalar", + "type": { + "option": "u8" + } + }, + { + "name": "optionalLpNetPnlTransferScalar", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "settleExpiredMarketPoolsToRevenuePool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositIntoPerpMarketFeePool", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "sourceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "quoteSpotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "updatePerpMarketPnlPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false }, { "name": "admin", @@ -5751,6 +6056,11 @@ "name": "perpMarket", "isMut": true, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -6143,6 +6453,11 @@ "name": "oldOracle", "isMut": false, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -7127,6 +7442,119 @@ } ] }, + { + "name": "initializeLpPool", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "minMintFee", + "type": "i64" + }, + { + "name": "maxAum", + "type": "u128" + }, + { + "name": "maxSettleQuoteAmountPerMarket", + "type": "u64" + }, + { + "name": "whitelistMint", + "type": "publicKey" + } + ] + }, + { + "name": "increaseLpPoolMaxAum", + "accounts": [ + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newMaxAum", + "type": "u128" + } + ] + }, { "name": "updateHighLeverageModeConfig", "accounts": [ @@ -7576,50 +8004,1664 @@ "type": "bool" } ] - } - ], - "accounts": [ + }, { - "name": "OpenbookV2FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "openbookV2ProgramId", - "type": "publicKey" - }, - { - "name": "openbookV2Market", - "type": "publicKey" - }, - { - "name": "openbookV2MarketAuthority", - "type": "publicKey" - }, - { - "name": "openbookV2EventHeap", - "type": "publicKey" - }, - { - "name": "openbookV2Bids", - "type": "publicKey" - }, - { - "name": "openbookV2Asks", - "type": "publicKey" - }, + "name": "updateFeatureBitFlagsSettleLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsSwapLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsMintRedeemLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "initializeConstituent", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentVault", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "maxWeightDeviation", + "type": "i64" + }, + { + "name": "swapFeeMin", + "type": "i64" + }, + { + "name": "swapFeeMax", + "type": "i64" + }, + { + "name": "maxBorrowTokenAmount", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", + "type": "u64" + }, + { + "name": "costToTrade", + "type": "i32" + }, + { + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "derivativeWeight", + "type": "u64" + }, + { + "name": "volatility", + "type": "u64" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "newConstituentCorrelations", + "type": { + "vec": "i64" + } + } + ] + }, + { + "name": "updateConstituentStatus", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "newStatus", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentParams", + "accounts": [ + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "constituentParams", + "type": { + "defined": "ConstituentParams" + } + } + ] + }, + { + "name": "updateLpPoolParams", + "accounts": [ + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolParams", + "type": { + "defined": "LpPoolParams" + } + } + ] + }, + { + "name": "addAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "updateAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "removeAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentCorrelationData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "index1", + "type": "u16" + }, + { + "name": "index2", + "type": "u16" + }, + { + "name": "correlation", + "type": "i64" + } + ] + }, + { + "name": "updateLpConstituentTargetBase", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammConstituentMapping", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateLpPoolAum", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateAmmCache", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "overrideAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "overrideParams", + "type": { + "defined": "OverrideAmmCacheParams" + } + } + ] + }, + { + "name": "resetAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "lpPoolSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u64" + } + ] + }, + { + "name": "viewLpPoolSwapFees", + "accounts": [ + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "inTargetWeight", + "type": "i64" + }, + { + "name": "outTargetWeight", + "type": "i64" + } + ] + }, + { + "name": "lpPoolAddLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + }, + { + "name": "minMintAmount", + "type": "u64" + } + ] + }, + { + "name": "lpPoolRemoveLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolAddLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolRemoveLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + } + ] + }, + { + "name": "beginLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" + } + ] + }, + { + "name": "endLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentOracleInfo", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositToProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdrawFromProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "settlePerpToLpPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentQuoteTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "AmmCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "cache", + "type": { + "vec": { + "defined": "CacheInfo" + } + } + } + ] + } + }, + { + "name": "OpenbookV2FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "openbookV2ProgramId", + "type": "publicKey" + }, + { + "name": "openbookV2Market", + "type": "publicKey" + }, + { + "name": "openbookV2MarketAuthority", + "type": "publicKey" + }, + { + "name": "openbookV2EventHeap", + "type": "publicKey" + }, + { + "name": "openbookV2Bids", + "type": "publicKey" + }, + { + "name": "openbookV2Asks", + "type": "publicKey" + }, + { + "name": "openbookV2BaseVault", + "type": "publicKey" + }, + { + "name": "openbookV2QuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PhoenixV1FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "phoenixProgramId", + "type": "publicKey" + }, + { + "name": "phoenixLogAuthority", + "type": "publicKey" + }, + { + "name": "phoenixMarket", + "type": "publicKey" + }, + { + "name": "phoenixBaseVault", + "type": "publicKey" + }, + { + "name": "phoenixQuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "SerumV3FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "serumProgramId", + "type": "publicKey" + }, + { + "name": "serumMarket", + "type": "publicKey" + }, { - "name": "openbookV2BaseVault", + "name": "serumRequestQueue", "type": "publicKey" }, { - "name": "openbookV2QuoteVault", + "name": "serumEventQueue", + "type": "publicKey" + }, + { + "name": "serumBids", + "type": "publicKey" + }, + { + "name": "serumAsks", + "type": "publicKey" + }, + { + "name": "serumBaseVault", + "type": "publicKey" + }, + { + "name": "serumQuoteVault", + "type": "publicKey" + }, + { + "name": "serumOpenOrders", "type": "publicKey" }, + { + "name": "serumSignerNonce", + "type": "u64" + }, { "name": "marketIndex", "type": "u16" @@ -7649,7 +9691,49 @@ } }, { - "name": "PhoenixV1FulfillmentConfig", + "name": "HighLeverageModeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxUsers", + "type": "u32" + }, + { + "name": "currentUsers", + "type": "u32" + }, + { + "name": "reduceOnly", + "type": "u8" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "currentMaintenanceUsers", + "type": "u32" + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 24 + ] + } + } + ] + } + }, + { + "name": "IfRebalanceConfig", "type": { "kind": "struct", "fields": [ @@ -7658,166 +9742,350 @@ "type": "publicKey" }, { - "name": "phoenixProgramId", - "type": "publicKey" + "name": "totalInAmount", + "docs": [ + "total amount to be sold" + ], + "type": "u64" }, { - "name": "phoenixLogAuthority", - "type": "publicKey" + "name": "currentInAmount", + "docs": [ + "amount already sold" + ], + "type": "u64" }, { - "name": "phoenixMarket", - "type": "publicKey" + "name": "currentOutAmount", + "docs": [ + "amount already bought" + ], + "type": "u64" }, { - "name": "phoenixBaseVault", - "type": "publicKey" + "name": "currentOutAmountTransferred", + "docs": [ + "amount already transferred to revenue pool" + ], + "type": "u64" }, { - "name": "phoenixQuoteVault", - "type": "publicKey" + "name": "currentInAmountSinceLastTransfer", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" }, { - "name": "marketIndex", + "name": "epochStartTs", + "docs": [ + "start time of epoch" + ], + "type": "i64" + }, + { + "name": "epochInAmount", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "docs": [ + "max amount to swap in epoch" + ], + "type": "u64" + }, + { + "name": "epochDuration", + "docs": [ + "duration of epoch" + ], + "type": "i64" + }, + { + "name": "outMarketIndex", + "docs": [ + "market index to sell" + ], "type": "u16" }, { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } + "name": "inMarketIndex", + "docs": [ + "market index to buy" + ], + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" }, { "name": "status", + "type": "u8" + }, + { + "name": "padding2", "type": { - "defined": "SpotFulfillmentConfigStatus" + "array": [ + "u8", + 32 + ] } + } + ] + } + }, + { + "name": "InsuranceFundStake", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "ifShares", + "type": "u128" + }, + { + "name": "lastWithdrawRequestShares", + "type": "u128" + }, + { + "name": "ifBase", + "type": "u128" + }, + { + "name": "lastValidTs", + "type": "i64" + }, + { + "name": "lastWithdrawRequestValue", + "type": "u64" + }, + { + "name": "lastWithdrawRequestTs", + "type": "i64" + }, + { + "name": "costBasis", + "type": "i64" + }, + { + "name": "marketIndex", + "type": "u16" }, { "name": "padding", "type": { "array": [ "u8", + 14 + ] + } + } + ] + } + }, + { + "name": "ProtocolIfSharesTransferConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whitelistedSigners", + "type": { + "array": [ + "publicKey", 4 ] } + }, + { + "name": "maxTransferPerEpoch", + "type": "u128" + }, + { + "name": "currentEpochTransfer", + "type": "u128" + }, + { + "name": "nextEpochTs", + "type": "i64" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 8 + ] + } } ] } }, { - "name": "SerumV3FulfillmentConfig", + "name": "LPPool", "type": { "kind": "struct", "fields": [ + { + "name": "name", + "docs": [ + "name of vault, TODO: check type + size" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, { "name": "pubkey", + "docs": [ + "address of the vault." + ], "type": "publicKey" }, { - "name": "serumProgramId", + "name": "mint", "type": "publicKey" }, { - "name": "serumMarket", + "name": "whitelistMint", "type": "publicKey" }, { - "name": "serumRequestQueue", + "name": "constituentTargetBase", "type": "publicKey" }, { - "name": "serumEventQueue", + "name": "constituentCorrelations", "type": "publicKey" }, { - "name": "serumBids", - "type": "publicKey" + "name": "maxAum", + "docs": [ + "The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index)", + "which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0)", + "pub quote_constituent_index: u16,", + "QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this" + ], + "type": "u128" }, { - "name": "serumAsks", - "type": "publicKey" + "name": "lastAum", + "docs": [ + "QUOTE_PRECISION: AUM of the vault in USD, updated lazily" + ], + "type": "u128" }, { - "name": "serumBaseVault", - "type": "publicKey" + "name": "cumulativeQuoteSentToPerpMarkets", + "docs": [ + "QUOTE PRECISION: Cumulative quotes from settles" + ], + "type": "u128" }, { - "name": "serumQuoteVault", - "type": "publicKey" + "name": "cumulativeQuoteReceivedFromPerpMarkets", + "type": "u128" }, { - "name": "serumOpenOrders", - "type": "publicKey" + "name": "totalMintRedeemFeesPaid", + "docs": [ + "QUOTE_PRECISION: Total fees paid for minting and redeeming LP tokens" + ], + "type": "i128" }, { - "name": "serumSignerNonce", + "name": "lastAumSlot", + "docs": [ + "timestamp of last AUM slot" + ], "type": "u64" }, { - "name": "marketIndex", - "type": "u16" + "name": "maxSettleQuoteAmount", + "type": "u64" }, { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } + "name": "lastHedgeTs", + "docs": [ + "timestamp of last vAMM revenue rebalance" + ], + "type": "u64" }, { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } + "name": "mintRedeemId", + "docs": [ + "Every mint/redeem has a monotonically increasing id. This is the next id to use" + ], + "type": "u64" }, { - "name": "padding", - "type": { - "array": [ - "u8", - 4 - ] - } - } - ] - } - }, - { - "name": "HighLeverageModeConfig", - "type": { - "kind": "struct", - "fields": [ + "name": "settleId", + "type": "u64" + }, { - "name": "maxUsers", - "type": "u32" + "name": "minMintFee", + "docs": [ + "PERCENTAGE_PRECISION" + ], + "type": "i64" }, { - "name": "currentUsers", - "type": "u32" + "name": "tokenSupply", + "type": "u64" }, { - "name": "reduceOnly", + "name": "volatility", + "type": "u64" + }, + { + "name": "constituents", + "type": "u16" + }, + { + "name": "quoteConsituentIndex", + "type": "u16" + }, + { + "name": "bump", "type": "u8" }, { - "name": "padding1", - "type": { - "array": [ - "u8", - 3 - ] - } + "name": "gammaExecution", + "type": "u8" }, { - "name": "currentMaintenanceUsers", - "type": "u32" + "name": "xi", + "type": "u8" }, { - "name": "padding2", + "name": "targetOracleDelayFeeBpsPer10Slots", + "type": "u8" + }, + { + "name": "targetPositionDelayFeeBpsPer10Slots", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ "u8", - 24 + 15 ] } } @@ -7825,97 +10093,173 @@ } }, { - "name": "IfRebalanceConfig", + "name": "Constituent", "type": { "kind": "struct", "fields": [ { "name": "pubkey", + "docs": [ + "address of the constituent" + ], "type": "publicKey" }, { - "name": "totalInAmount", + "name": "mint", + "type": "publicKey" + }, + { + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "vault", + "type": "publicKey" + }, + { + "name": "totalSwapFees", "docs": [ - "total amount to be sold" + "total fees received by the constituent. Positive = fees received, Negative = fees paid" ], - "type": "u64" + "type": "i128" }, { - "name": "currentInAmount", + "name": "spotBalance", "docs": [ - "amount already sold" + "spot borrow-lend balance for constituent" ], - "type": "u64" + "type": { + "defined": "ConstituentSpotBalance" + } }, { - "name": "currentOutAmount", + "name": "lastSpotBalanceTokenAmount", + "type": "i64" + }, + { + "name": "cumulativeSpotInterestAccruedTokenAmount", + "type": "i64" + }, + { + "name": "maxWeightDeviation", "docs": [ - "amount already bought" + "max deviation from target_weight allowed for the constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentOutAmountTransferred", + "name": "swapFeeMin", "docs": [ - "amount already transferred to revenue pool" + "min fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentInAmountSinceLastTransfer", + "name": "swapFeeMax", "docs": [ - "amount already bought in epoch" + "max fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "epochStartTs", + "name": "maxBorrowTokenAmount", "docs": [ - "start time of epoch" + "Max Borrow amount:", + "precision: token precision" ], - "type": "i64" + "type": "u64" }, { - "name": "epochInAmount", + "name": "vaultTokenBalance", "docs": [ - "amount already bought in epoch" + "ata token balance in token precision" ], "type": "u64" }, { - "name": "epochMaxInAmount", + "name": "lastOraclePrice", + "type": "i64" + }, + { + "name": "lastOracleSlot", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", "docs": [ - "max amount to swap in epoch" + "Delay allowed for valid AUM calculation" ], "type": "u64" }, { - "name": "epochDuration", + "name": "flashLoanInitialTokenAmount", + "type": "u64" + }, + { + "name": "nextSwapId", "docs": [ - "duration of epoch" + "Every swap to/from this constituent has a monotonically increasing id. This is the next id to use" ], - "type": "i64" + "type": "u64" }, { - "name": "outMarketIndex", + "name": "derivativeWeight", "docs": [ - "market index to sell" + "percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight" ], - "type": "u16" + "type": "u64" }, { - "name": "inMarketIndex", + "name": "volatility", + "type": "u64" + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "constituentDerivativeIndex", "docs": [ - "market index to buy" + "The `constituent_index` of the parent constituent. -1 if it is a parent index", + "Example: if in a pool with SOL (parent) and dSOL (derivative),", + "SOL.constituent_index = 1, SOL.constituent_derivative_index = -1,", + "dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1" ], + "type": "i16" + }, + { + "name": "spotMarketIndex", "type": "u16" }, { - "name": "maxSlippageBps", + "name": "constituentIndex", "type": "u16" }, { - "name": "swapMode", + "name": "decimals", + "type": "u8" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "vaultBump", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", "type": "u8" }, { @@ -7923,11 +10267,15 @@ "type": "u8" }, { - "name": "padding2", + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ "u8", - 32 + 2 ] } } @@ -7935,92 +10283,98 @@ } }, { - "name": "InsuranceFundStake", + "name": "AmmConstituentMapping", "type": { "kind": "struct", "fields": [ { - "name": "authority", + "name": "lpPool", "type": "publicKey" }, { - "name": "ifShares", - "type": "u128" - }, - { - "name": "lastWithdrawRequestShares", - "type": "u128" - }, - { - "name": "ifBase", - "type": "u128" - }, - { - "name": "lastValidTs", - "type": "i64" + "name": "bump", + "type": "u8" }, { - "name": "lastWithdrawRequestValue", - "type": "u64" + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "lastWithdrawRequestTs", - "type": "i64" - }, + "name": "weights", + "type": { + "vec": { + "defined": "AmmConstituentDatum" + } + } + } + ] + } + }, + { + "name": "ConstituentTargetBase", + "type": { + "kind": "struct", + "fields": [ { - "name": "costBasis", - "type": "i64" + "name": "lpPool", + "type": "publicKey" }, { - "name": "marketIndex", - "type": "u16" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ "u8", - 14 + 3 ] } + }, + { + "name": "targets", + "type": { + "vec": { + "defined": "TargetsDatum" + } + } } ] } }, { - "name": "ProtocolIfSharesTransferConfig", + "name": "ConstituentCorrelations", "type": { "kind": "struct", "fields": [ { - "name": "whitelistedSigners", - "type": { - "array": [ - "publicKey", - 4 - ] - } - }, - { - "name": "maxTransferPerEpoch", - "type": "u128" - }, - { - "name": "currentEpochTransfer", - "type": "u128" + "name": "lpPool", + "type": "publicKey" }, { - "name": "nextEpochTs", - "type": "i64" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 8 + "u8", + 3 ] } + }, + { + "name": "correlations", + "type": { + "vec": "i64" + } } ] } @@ -8343,8 +10697,20 @@ "type": "u8" }, { - "name": "padding1", - "type": "u32" + "name": "lpFeeTransferScalar", + "type": "u8" + }, + { + "name": "lpStatus", + "type": "u8" + }, + { + "name": "lpPausedOperations", + "type": "u8" + }, + { + "name": "lpExchangeFeeExcluscionScalar", + "type": "u8" }, { "name": "lastFillPrice", @@ -9110,91 +11476,352 @@ } }, { - "name": "spotFeeStructure", - "type": { - "defined": "FeeStructure" - } + "name": "spotFeeStructure", + "type": { + "defined": "FeeStructure" + } + }, + { + "name": "oracleGuardRails", + "type": { + "defined": "OracleGuardRails" + } + }, + { + "name": "numberOfAuthorities", + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "type": "u64" + }, + { + "name": "lpCooldownTime", + "type": "u64" + }, + { + "name": "liquidationMarginBufferRatio", + "type": "u32" + }, + { + "name": "settlementDuration", + "type": "u16" + }, + { + "name": "numberOfMarkets", + "type": "u16" + }, + { + "name": "numberOfSpotMarkets", + "type": "u16" + }, + { + "name": "signerNonce", + "type": "u8" + }, + { + "name": "minPerpAuctionDuration", + "type": "u8" + }, + { + "name": "defaultMarketOrderTimeInForce", + "type": "u8" + }, + { + "name": "defaultSpotAuctionDuration", + "type": "u8" + }, + { + "name": "exchangeStatus", + "type": "u8" + }, + { + "name": "liquidationDuration", + "type": "u8" + }, + { + "name": "initialPctToLiquidate", + "type": "u16" + }, + { + "name": "maxNumberOfSubAccounts", + "type": "u16" + }, + { + "name": "maxInitializeUserFee", + "type": "u16" + }, + { + "name": "featureBitFlags", + "type": "u8" + }, + { + "name": "lpPoolFeatureBitFlags", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + }, + { + "name": "User", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The owner/authority of the account" + ], + "type": "publicKey" + }, + { + "name": "delegate", + "docs": [ + "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" + ], + "type": "publicKey" + }, + { + "name": "name", + "docs": [ + "Encoded display name e.g. \"toly\"" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "spotPositions", + "docs": [ + "The user's spot positions" + ], + "type": { + "array": [ + { + "defined": "SpotPosition" + }, + 8 + ] + } + }, + { + "name": "perpPositions", + "docs": [ + "The user's perp positions" + ], + "type": { + "array": [ + { + "defined": "PerpPosition" + }, + 8 + ] + } + }, + { + "name": "orders", + "docs": [ + "The user's orders" + ], + "type": { + "array": [ + { + "defined": "Order" + }, + 32 + ] + } + }, + { + "name": "lastAddPerpLpSharesTs", + "docs": [ + "The last time the user added perp lp positions" + ], + "type": "i64" + }, + { + "name": "totalDeposits", + "docs": [ + "The total values of deposits the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalWithdraws", + "docs": [ + "The total values of withdrawals the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalSocialLoss", + "docs": [ + "The total socialized loss the users has incurred upon the protocol", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "settledPerpPnl", + "docs": [ + "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" }, { - "name": "oracleGuardRails", - "type": { - "defined": "OracleGuardRails" - } + "name": "cumulativeSpotFees", + "docs": [ + "Fees (taker fees, maker rebate, filler reward) for spot", + "precision: QUOTE_PRECISION" + ], + "type": "i64" }, { - "name": "numberOfAuthorities", - "type": "u64" + "name": "cumulativePerpFunding", + "docs": [ + "Cumulative funding paid/received for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" }, { - "name": "numberOfSubAccounts", + "name": "liquidationMarginFreed", + "docs": [ + "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", + "Defaults to zero when not being liquidated", + "precision: QUOTE_PRECISION" + ], "type": "u64" }, { - "name": "lpCooldownTime", + "name": "lastActiveSlot", + "docs": [ + "The last slot a user was active. Used to determine if a user is idle" + ], "type": "u64" }, { - "name": "liquidationMarginBufferRatio", + "name": "nextOrderId", + "docs": [ + "Every user order has an order id. This is the next order id to be used" + ], "type": "u32" }, { - "name": "settlementDuration", - "type": "u16" + "name": "maxMarginRatio", + "docs": [ + "Custom max initial margin ratio for the user" + ], + "type": "u32" }, { - "name": "numberOfMarkets", + "name": "nextLiquidationId", + "docs": [ + "The next liquidation id to be used for user" + ], "type": "u16" }, { - "name": "numberOfSpotMarkets", + "name": "subAccountId", + "docs": [ + "The sub account id for this user" + ], "type": "u16" }, { - "name": "signerNonce", + "name": "status", + "docs": [ + "Whether the user is active, being liquidated or bankrupt" + ], "type": "u8" }, { - "name": "minPerpAuctionDuration", - "type": "u8" + "name": "isMarginTradingEnabled", + "docs": [ + "Whether the user has enabled margin trading" + ], + "type": "bool" }, { - "name": "defaultMarketOrderTimeInForce", - "type": "u8" + "name": "idle", + "docs": [ + "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", + "Off-chain keeper bots can ignore users that are idle" + ], + "type": "bool" }, { - "name": "defaultSpotAuctionDuration", + "name": "openOrders", + "docs": [ + "number of open orders" + ], "type": "u8" }, { - "name": "exchangeStatus", - "type": "u8" + "name": "hasOpenOrder", + "docs": [ + "Whether or not user has open order" + ], + "type": "bool" }, { - "name": "liquidationDuration", + "name": "openAuctions", + "docs": [ + "number of open orders with auction" + ], "type": "u8" }, { - "name": "initialPctToLiquidate", - "type": "u16" + "name": "hasOpenAuction", + "docs": [ + "Whether or not user has open order with auction" + ], + "type": "bool" }, { - "name": "maxNumberOfSubAccounts", - "type": "u16" + "name": "marginMode", + "type": { + "defined": "MarginMode" + } }, { - "name": "maxInitializeUserFee", - "type": "u16" + "name": "poolId", + "type": "u8" }, { - "name": "featureBitFlags", - "type": "u8" + "name": "padding1", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "lastFuelBonusUpdateTs", + "type": "u32" }, { "name": "padding", "type": { "array": [ "u8", - 9 + 12 ] } } @@ -9202,577 +11829,623 @@ } }, { - "name": "User", + "name": "UserStats", "type": { "kind": "struct", "fields": [ { "name": "authority", "docs": [ - "The owner/authority of the account" + "The authority for all of a users sub accounts" ], "type": "publicKey" }, { - "name": "delegate", + "name": "referrer", "docs": [ - "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" + "The address that referred this user" ], "type": "publicKey" }, { - "name": "name", + "name": "fees", "docs": [ - "Encoded display name e.g. \"toly\"" + "Stats on the fees paid by the user" ], "type": { - "array": [ - "u8", - 32 - ] + "defined": "UserFees" } }, { - "name": "spotPositions", + "name": "nextEpochTs", "docs": [ - "The user's spot positions" + "The timestamp of the next epoch", + "Epoch is used to limit referrer rewards earned in single epoch" ], - "type": { - "array": [ - { - "defined": "SpotPosition" - }, - 8 - ] - } + "type": "i64" }, { - "name": "perpPositions", + "name": "makerVolume30d", "docs": [ - "The user's perp positions" + "Rolling 30day maker volume for user", + "precision: QUOTE_PRECISION" ], - "type": { - "array": [ - { - "defined": "PerpPosition" - }, - 8 - ] - } + "type": "u64" }, { - "name": "orders", + "name": "takerVolume30d", "docs": [ - "The user's orders" + "Rolling 30day taker volume for user", + "precision: QUOTE_PRECISION" ], - "type": { - "array": [ - { - "defined": "Order" - }, - 32 + "type": "u64" + }, + { + "name": "fillerVolume30d", + "docs": [ + "Rolling 30day filler volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMakerVolume30dTs", + "docs": [ + "last time the maker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastTakerVolume30dTs", + "docs": [ + "last time the taker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastFillerVolume30dTs", + "docs": [ + "last time the filler volume was updated" + ], + "type": "i64" + }, + { + "name": "ifStakedQuoteAssetAmount", + "docs": [ + "The amount of tokens staked in the quote spot markets if" + ], + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "docs": [ + "The current number of sub accounts" + ], + "type": "u16" + }, + { + "name": "numberOfSubAccountsCreated", + "docs": [ + "The number of sub accounts created. Can be greater than the number of sub accounts if user", + "has deleted sub accounts" + ], + "type": "u16" + }, + { + "name": "referrerStatus", + "docs": [ + "Flags for referrer status:", + "First bit (LSB): 1 if user is a referrer, 0 otherwise", + "Second bit: 1 if user was referred, 0 otherwise" + ], + "type": "u8" + }, + { + "name": "disableUpdatePerpBidAskTwap", + "type": "bool" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 1 ] } }, { - "name": "lastAddPerpLpSharesTs", + "name": "fuelOverflowStatus", "docs": [ - "The last time the user added perp lp positions" + "whether the user has a FuelOverflow account" ], - "type": "i64" + "type": "u8" }, { - "name": "totalDeposits", + "name": "fuelInsurance", "docs": [ - "The total values of deposits the user has made", - "precision: QUOTE_PRECISION" + "accumulated fuel for token amounts of insurance" ], - "type": "u64" + "type": "u32" }, { - "name": "totalWithdraws", + "name": "fuelDeposits", "docs": [ - "The total values of withdrawals the user has made", - "precision: QUOTE_PRECISION" + "accumulated fuel for notional of deposits" ], - "type": "u64" + "type": "u32" }, { - "name": "totalSocialLoss", + "name": "fuelBorrows", "docs": [ - "The total socialized loss the users has incurred upon the protocol", - "precision: QUOTE_PRECISION" + "accumulate fuel bonus for notional of borrows" ], - "type": "u64" + "type": "u32" }, { - "name": "settledPerpPnl", + "name": "fuelPositions", "docs": [ - "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", - "precision: QUOTE_PRECISION" + "accumulated fuel for perp open interest" ], - "type": "i64" + "type": "u32" }, { - "name": "cumulativeSpotFees", + "name": "fuelTaker", "docs": [ - "Fees (taker fees, maker rebate, filler reward) for spot", - "precision: QUOTE_PRECISION" + "accumulate fuel bonus for taker volume" ], - "type": "i64" + "type": "u32" }, { - "name": "cumulativePerpFunding", + "name": "fuelMaker", "docs": [ - "Cumulative funding paid/received for perps", - "precision: QUOTE_PRECISION" + "accumulate fuel bonus for maker volume" ], - "type": "i64" + "type": "u32" }, { - "name": "liquidationMarginFreed", + "name": "ifStakedGovTokenAmount", "docs": [ - "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", - "Defaults to zero when not being liquidated", - "precision: QUOTE_PRECISION" + "The amount of tokens staked in the governance spot markets if" ], "type": "u64" }, { - "name": "lastActiveSlot", + "name": "lastFuelIfBonusUpdateTs", "docs": [ - "The last slot a user was active. Used to determine if a user is idle" + "last unix ts user stats data was used to update if fuel (u32 to save space)" ], - "type": "u64" + "type": "u32" }, { - "name": "nextOrderId", + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "ReferrerName", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "user", + "type": "publicKey" + }, + { + "name": "userStats", + "type": "publicKey" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "FuelOverflow", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", "docs": [ - "Every user order has an order id. This is the next order id to be used" + "The authority of this overflow account" ], + "type": "publicKey" + }, + { + "name": "fuelInsurance", + "type": "u128" + }, + { + "name": "fuelDeposits", + "type": "u128" + }, + { + "name": "fuelBorrows", + "type": "u128" + }, + { + "name": "fuelPositions", + "type": "u128" + }, + { + "name": "fuelTaker", + "type": "u128" + }, + { + "name": "fuelMaker", + "type": "u128" + }, + { + "name": "lastFuelSweepTs", "type": "u32" }, { - "name": "maxMarginRatio", - "docs": [ - "Custom max initial margin ratio for the user" - ], + "name": "lastResetTs", "type": "u32" }, { - "name": "nextLiquidationId", - "docs": [ - "The next liquidation id to be used for user" - ], - "type": "u16" + "name": "padding", + "type": { + "array": [ + "u128", + 6 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } }, { - "name": "subAccountId", - "docs": [ - "The sub account id for this user" - ], - "type": "u16" + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } }, { - "name": "status", - "docs": [ - "Whether the user is active, being liquidated or bankrupt" - ], - "type": "u8" + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } }, { - "name": "isMarginTradingEnabled", - "docs": [ - "Whether the user has enabled margin trading" - ], - "type": "bool" + "name": "excludeTotalLiqFee", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "ConstituentParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxWeightDeviation", + "type": { + "option": "i64" + } }, { - "name": "idle", - "docs": [ - "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", - "Off-chain keeper bots can ignore users that are idle" - ], - "type": "bool" + "name": "swapFeeMin", + "type": { + "option": "i64" + } }, { - "name": "openOrders", - "docs": [ - "number of open orders" - ], - "type": "u8" + "name": "swapFeeMax", + "type": { + "option": "i64" + } }, { - "name": "hasOpenOrder", - "docs": [ - "Whether or not user has open order" - ], - "type": "bool" + "name": "maxBorrowTokenAmount", + "type": { + "option": "u64" + } }, { - "name": "openAuctions", - "docs": [ - "number of open orders with auction" - ], - "type": "u8" + "name": "oracleStalenessThreshold", + "type": { + "option": "u64" + } }, { - "name": "hasOpenAuction", - "docs": [ - "Whether or not user has open order with auction" - ], - "type": "bool" + "name": "costToTradeBps", + "type": { + "option": "i32" + } }, { - "name": "marginMode", + "name": "constituentDerivativeIndex", "type": { - "defined": "MarginMode" + "option": "i16" } }, { - "name": "poolId", - "type": "u8" + "name": "derivativeWeight", + "type": { + "option": "u64" + } }, { - "name": "padding1", + "name": "volatility", "type": { - "array": [ - "u8", - 3 - ] + "option": "u64" } }, { - "name": "lastFuelBonusUpdateTs", - "type": "u32" + "name": "gammaExecution", + "type": { + "option": "u8" + } }, { - "name": "padding", + "name": "gammaInventory", "type": { - "array": [ - "u8", - 12 - ] + "option": "u8" + } + }, + { + "name": "xi", + "type": { + "option": "u8" } } ] } }, { - "name": "UserStats", + "name": "LpPoolParams", "type": { "kind": "struct", "fields": [ { - "name": "authority", - "docs": [ - "The authority for all of a users sub accounts" - ], - "type": "publicKey" + "name": "maxSettleQuoteAmount", + "type": { + "option": "u64" + } }, { - "name": "referrer", - "docs": [ - "The address that referred this user" - ], - "type": "publicKey" + "name": "volatility", + "type": { + "option": "u64" + } }, { - "name": "fees", - "docs": [ - "Stats on the fees paid by the user" - ], + "name": "gammaExecution", "type": { - "defined": "UserFees" + "option": "u8" } }, { - "name": "nextEpochTs", - "docs": [ - "The timestamp of the next epoch", - "Epoch is used to limit referrer rewards earned in single epoch" - ], - "type": "i64" + "name": "xi", + "type": { + "option": "u8" + } }, { - "name": "makerVolume30d", - "docs": [ - "Rolling 30day maker volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" - }, + "name": "whitelistMint", + "type": { + "option": "publicKey" + } + } + ] + } + }, + { + "name": "OverrideAmmCacheParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "takerVolume30d", - "docs": [ - "Rolling 30day taker volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "quoteOwedFromLpPool", + "type": { + "option": "i64" + } }, { - "name": "fillerVolume30d", - "docs": [ - "Rolling 30day filler volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "lastSettleSlot", + "type": { + "option": "u64" + } }, { - "name": "lastMakerVolume30dTs", - "docs": [ - "last time the maker volume was updated" - ], - "type": "i64" + "name": "lastFeePoolTokenAmount", + "type": { + "option": "u128" + } }, { - "name": "lastTakerVolume30dTs", - "docs": [ - "last time the taker volume was updated" - ], - "type": "i64" + "name": "lastNetPnlPoolTokenAmount", + "type": { + "option": "i128" + } }, { - "name": "lastFillerVolume30dTs", - "docs": [ - "last time the filler volume was updated" - ], - "type": "i64" + "name": "ammPositionScalar", + "type": { + "option": "u8" + } }, { - "name": "ifStakedQuoteAssetAmount", - "docs": [ - "The amount of tokens staked in the quote spot markets if" - ], - "type": "u64" - }, + "name": "ammInventoryLimit", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "AddAmmConstituentMappingDatum", + "type": { + "kind": "struct", + "fields": [ { - "name": "numberOfSubAccounts", - "docs": [ - "The current number of sub accounts" - ], + "name": "constituentIndex", "type": "u16" }, { - "name": "numberOfSubAccountsCreated", - "docs": [ - "The number of sub accounts created. Can be greater than the number of sub accounts if user", - "has deleted sub accounts" - ], + "name": "perpMarketIndex", "type": "u16" }, { - "name": "referrerStatus", - "docs": [ - "Flags for referrer status:", - "First bit (LSB): 1 if user is a referrer, 0 otherwise", - "Second bit: 1 if user was referred, 0 otherwise" - ], - "type": "u8" - }, - { - "name": "disableUpdatePerpBidAskTwap", - "type": "bool" - }, - { - "name": "padding1", - "type": { - "array": [ - "u8", - 1 - ] - } - }, + "name": "weight", + "type": "i64" + } + ] + } + }, + { + "name": "CacheInfo", + "type": { + "kind": "struct", + "fields": [ { - "name": "fuelOverflowStatus", - "docs": [ - "whether the user has a FuelOverflow account" - ], - "type": "u8" + "name": "oracle", + "type": "publicKey" }, { - "name": "fuelInsurance", - "docs": [ - "accumulated fuel for token amounts of insurance" - ], - "type": "u32" + "name": "lastFeePoolTokenAmount", + "type": "u128" }, { - "name": "fuelDeposits", - "docs": [ - "accumulated fuel for notional of deposits" - ], - "type": "u32" + "name": "lastNetPnlPoolTokenAmount", + "type": "i128" }, { - "name": "fuelBorrows", - "docs": [ - "accumulate fuel bonus for notional of borrows" - ], - "type": "u32" + "name": "lastExchangeFees", + "type": "u128" }, { - "name": "fuelPositions", - "docs": [ - "accumulated fuel for perp open interest" - ], - "type": "u32" + "name": "lastSettleAmmExFees", + "type": "u128" }, { - "name": "fuelTaker", - "docs": [ - "accumulate fuel bonus for taker volume" - ], - "type": "u32" + "name": "lastSettleAmmPnl", + "type": "i128" }, { - "name": "fuelMaker", + "name": "position", "docs": [ - "accumulate fuel bonus for maker volume" + "BASE PRECISION" ], - "type": "u32" + "type": "i64" }, { - "name": "ifStakedGovTokenAmount", - "docs": [ - "The amount of tokens staked in the governance spot markets if" - ], + "name": "slot", "type": "u64" }, { - "name": "lastFuelIfBonusUpdateTs", - "docs": [ - "last unix ts user stats data was used to update if fuel (u32 to save space)" - ], - "type": "u32" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 12 - ] - } - } - ] - } - }, - { - "name": "ReferrerName", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "publicKey" - }, - { - "name": "user", - "type": "publicKey" - }, - { - "name": "userStats", - "type": "publicKey" + "name": "lastSettleAmount", + "type": "u64" }, { - "name": "name", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "FuelOverflow", - "type": { - "kind": "struct", - "fields": [ + "name": "lastSettleSlot", + "type": "u64" + }, { - "name": "authority", - "docs": [ - "The authority of this overflow account" - ], - "type": "publicKey" + "name": "lastSettleTs", + "type": "i64" }, { - "name": "fuelInsurance", - "type": "u128" + "name": "quoteOwedFromLpPool", + "type": "i64" }, { - "name": "fuelDeposits", - "type": "u128" + "name": "ammInventoryLimit", + "type": "i64" }, { - "name": "fuelBorrows", - "type": "u128" + "name": "oraclePrice", + "type": "i64" }, { - "name": "fuelPositions", - "type": "u128" + "name": "oracleSlot", + "type": "u64" }, { - "name": "fuelTaker", - "type": "u128" + "name": "oracleSource", + "type": "u8" }, { - "name": "fuelMaker", - "type": "u128" + "name": "oracleValidity", + "type": "u8" }, { - "name": "lastFuelSweepTs", - "type": "u32" + "name": "lpStatusForPerpMarket", + "type": "u8" }, { - "name": "lastResetTs", - "type": "u32" + "name": "ammPositionScalar", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 6 + "u8", + 36 ] } } ] } - } - ], - "types": [ + }, { - "name": "UpdatePerpMarketSummaryStatsParams", + "name": "AmmCacheFixed", "type": { "kind": "struct", "fields": [ { - "name": "quoteAssetAmountWithUnsettledLp", - "type": { - "option": "i64" - } - }, - { - "name": "netUnsettledFundingPnl", - "type": { - "option": "i64" - } + "name": "bump", + "type": "u8" }, { - "name": "updateAmmSummaryStats", + "name": "pad", "type": { - "option": "bool" + "array": [ + "u8", + 3 + ] } }, { - "name": "excludeTotalLiqFee", - "type": { - "option": "bool" - } + "name": "len", + "type": "u32" } ] } @@ -9994,48 +12667,260 @@ "type": "u128" }, { - "name": "cumulativeDepositInterestDelta", - "type": "u128" + "name": "cumulativeDepositInterestDelta", + "type": "u128" + } + ] + } + }, + { + "name": "IfRebalanceConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "totalInAmount", + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "type": "u64" + }, + { + "name": "epochDuration", + "type": "i64" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" + }, + { + "name": "status", + "type": "u8" + } + ] + } + }, + { + "name": "ConstituentSpotBalance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "scaledBalance", + "docs": [ + "The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow", + "interest of corresponding market.", + "precision: token precision" + ], + "type": "u128" + }, + { + "name": "cumulativeDeposits", + "docs": [ + "The cumulative deposits/borrows a user has made into a market", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "marketIndex", + "docs": [ + "The market index of the corresponding spot market" + ], + "type": "u16" + }, + { + "name": "balanceType", + "docs": [ + "Whether the position is deposit or borrow" + ], + "type": { + "defined": "SpotBalanceType" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 5 + ] + } + } + ] + } + }, + { + "name": "AmmConstituentDatum", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "lastSlot", + "type": "u64" + }, + { + "name": "weight", + "docs": [ + "PERCENTAGE_PRECISION. The weight this constituent has on the perp market" + ], + "type": "i64" + } + ] + } + }, + { + "name": "AmmConstituentMappingFixed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "len", + "type": "u32" + } + ] + } + }, + { + "name": "TargetsDatum", + "type": { + "kind": "struct", + "fields": [ + { + "name": "costToTradeBps", + "type": "i32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "targetBase", + "type": "i64" + }, + { + "name": "lastOracleSlot", + "type": "u64" + }, + { + "name": "lastPositionSlot", + "type": "u64" } ] } }, { - "name": "IfRebalanceConfigParams", + "name": "ConstituentTargetBaseFixed", "type": { "kind": "struct", "fields": [ { - "name": "totalInAmount", - "type": "u64" + "name": "lpPool", + "type": "publicKey" }, { - "name": "epochMaxInAmount", - "type": "u64" + "name": "bump", + "type": "u8" }, { - "name": "epochDuration", - "type": "i64" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "outMarketIndex", - "type": "u16" - }, + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" + } + ] + } + }, + { + "name": "ConstituentCorrelationsFixed", + "type": { + "kind": "struct", + "fields": [ { - "name": "inMarketIndex", - "type": "u16" + "name": "lpPool", + "type": "publicKey" }, { - "name": "maxSlippageBps", - "type": "u16" + "name": "bump", + "type": "u8" }, { - "name": "swapMode", - "type": "u8" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "status", - "type": "u8" + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" } ] } @@ -10334,6 +13219,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -10399,6 +13290,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -11863,13 +14760,13 @@ "type": "u64" }, { - "name": "lastBaseAssetAmountPerLp", + "name": "isolatedPositionScaledBalance", "docs": [ "The last base asset amount per lp the amm had", "Used to settle the users lp position", - "precision: BASE_PRECISION" + "precision: SPOT_BALANCE_PRECISION" ], - "type": "i64" + "type": "u64" }, { "name": "lastQuoteAssetAmountPerLp", @@ -11908,8 +14805,8 @@ "type": "u8" }, { - "name": "perLpBase", - "type": "i8" + "name": "positionFlag", + "type": "u8" } ] } @@ -12240,6 +15137,23 @@ ] } }, + { + "name": "SettlementDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ToLpPool" + }, + { + "name": "FromLpPool" + }, + { + "name": "None" + } + ] + } + }, { "name": "MarginRequirementType", "type": { @@ -12323,6 +15237,15 @@ }, { "name": "UseMMOraclePrice" + }, + { + "name": "UpdateAmmCache" + }, + { + "name": "UpdateLpPoolAum" + }, + { + "name": "LpPoolSwap" } ] } @@ -12547,6 +15470,17 @@ ] } }, + { + "name": "LiquidationBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + } + ] + } + }, { "name": "SettlePnlExplanation", "type": { @@ -12656,19 +15590,47 @@ ] } }, + { + "name": "ConstituentStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ReduceOnly" + }, + { + "name": "Decommissioned" + } + ] + } + }, + { + "name": "WeightValidationFlags", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NONE" + }, + { + "name": "EnforceTotalWeight100" + }, + { + "name": "NoNegativeWeights" + }, + { + "name": "NoOverweight" + } + ] + } + }, { "name": "MarginCalculationMode", "type": { "kind": "enum", "variants": [ { - "name": "Standard", - "fields": [ - { - "name": "trackOpenOrdersFraction", - "type": "bool" - } - ] + "name": "Standard" }, { "name": "Liquidation", @@ -12752,9 +15714,6 @@ }, { "name": "UpdateHighLeverageMode" - }, - { - "name": "EnableAutoTransfer" } ] } @@ -12879,6 +15838,37 @@ ] } }, + { + "name": "PerpLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TrackAmmRevenue" + }, + { + "name": "SettleQuoteOwed" + } + ] + } + }, + { + "name": "ConstituentLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Swap" + }, + { + "name": "Deposit" + }, + { + "name": "Withdraw" + } + ] + } + }, { "name": "MarketStatus", "type": { @@ -12914,6 +15904,23 @@ ] } }, + { + "name": "LpStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Uncollateralized" + }, + { + "name": "Active" + }, + { + "name": "Decommissioning" + } + ] + } + }, { "name": "ContractType", "type": { @@ -13125,6 +16132,23 @@ ] } }, + { + "name": "LpPoolFeatureBitFlags", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SettleLpPool" + }, + { + "name": "SwapLpPool" + }, + { + "name": "MintRedeemLpPool" + } + ] + } + }, { "name": "UserStatus", "type": { @@ -13262,6 +16286,23 @@ ] } }, + { + "name": "PositionFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + }, + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + } + ] + } + }, { "name": "ReferrerStatus", "type": { @@ -13481,6 +16522,13 @@ "option": "publicKey" }, "index": false + }, + { + "name": "signer", + "type": { + "option": "publicKey" + }, + "index": false } ] }, @@ -14226,6 +17274,11 @@ "defined": "SpotBankruptcyRecord" }, "index": false + }, + { + "name": "bitFlags", + "type": "u8", + "index": false } ] }, @@ -15662,8 +18715,8 @@ }, { "code": 6094, - "name": "CantUpdatePoolBalanceType", - "msg": "CantUpdatePoolBalanceType" + "name": "CantUpdateSpotBalanceType", + "msg": "CantUpdateSpotBalanceType" }, { "code": 6095, @@ -16814,6 +19867,116 @@ "code": 6324, "name": "UnableToLoadRevenueShareAccount", "msg": "Unable to load builder account" + }, + { + "code": 6325, + "name": "InvalidConstituent", + "msg": "Invalid Constituent" + }, + { + "code": 6326, + "name": "InvalidAmmConstituentMappingArgument", + "msg": "Invalid Amm Constituent Mapping argument" + }, + { + "code": 6327, + "name": "InvalidUpdateConstituentTargetBaseArgument", + "msg": "Invalid update constituent update target weights argument" + }, + { + "code": 6328, + "name": "ConstituentNotFound", + "msg": "Constituent not found" + }, + { + "code": 6329, + "name": "ConstituentCouldNotLoad", + "msg": "Constituent could not load" + }, + { + "code": 6330, + "name": "ConstituentWrongMutability", + "msg": "Constituent wrong mutability" + }, + { + "code": 6331, + "name": "WrongNumberOfConstituents", + "msg": "Wrong number of constituents passed to instruction" + }, + { + "code": 6332, + "name": "OracleTooStaleForLPAUMUpdate", + "msg": "Oracle too stale for LP AUM update" + }, + { + "code": 6333, + "name": "InsufficientConstituentTokenBalance", + "msg": "Insufficient constituent token balance" + }, + { + "code": 6334, + "name": "AMMCacheStale", + "msg": "Amm Cache data too stale" + }, + { + "code": 6335, + "name": "LpPoolAumDelayed", + "msg": "LP Pool AUM not updated recently" + }, + { + "code": 6336, + "name": "ConstituentOracleStale", + "msg": "Constituent oracle is stale" + }, + { + "code": 6337, + "name": "LpInvariantFailed", + "msg": "LP Invariant failed" + }, + { + "code": 6338, + "name": "InvalidConstituentDerivativeWeights", + "msg": "Invalid constituent derivative weights" + }, + { + "code": 6339, + "name": "UnauthorizedDlpAuthority", + "msg": "Unauthorized dlp authority" + }, + { + "code": 6340, + "name": "MaxDlpAumBreached", + "msg": "Max DLP AUM Breached" + }, + { + "code": 6341, + "name": "SettleLpPoolDisabled", + "msg": "Settle Lp Pool Disabled" + }, + { + "code": 6342, + "name": "MintRedeemLpPoolDisabled", + "msg": "Mint/Redeem Lp Pool Disabled" + }, + { + "code": 6343, + "name": "LpPoolSettleInvariantBreached", + "msg": "Settlement amount exceeded" + }, + { + "code": 6344, + "name": "InvalidConstituentOperation", + "msg": "Invalid constituent operation" + }, + { + "code": 6345, + "name": "Unauthorized", + "msg": "Unauthorized for operation" + }, + { + "code": 6346, + "name": "InvalidIsolatedPerpMarket", + "msg": "Invalid Isolated Perp Market" } ] } \ No newline at end of file diff --git a/sdk/src/types.ts b/sdk/src/types.ts index f3b3dc730b..b4653d0fa9 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1741,7 +1741,8 @@ export type AmmConstituentMapping = { export type TargetDatum = { costToTradeBps: number; - lastSlot: BN; + lastOracleSlot: BN; + lastPositionSlot: BN; targetBase: BN; }; From fe55733aa3205076cc07c54efcde120331b5371e Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:01:34 +0000 Subject: [PATCH 125/159] sdk: release v2.142.0-beta.27 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 84d536deff..3d464037fe 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.26 \ No newline at end of file +2.142.0-beta.27 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 3e6fc727b6..f823f96f68 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.26", + "version": "2.142.0-beta.27", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 9e3e00a627330cb61b1f8c5f70bbaedaa05cfd94 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 14 Oct 2025 17:36:58 -0400 Subject: [PATCH 126/159] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f92d2a3d5d..11a4c22069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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)) ### Fixes From 878e3bac11d426f98ed9337978094371ae3eeb4c Mon Sep 17 00:00:00 2001 From: lil perp Date: Tue, 14 Oct 2025 17:37:15 -0400 Subject: [PATCH 127/159] program: allow hot wallet to increase max spread and pause funding (#1957) * program: allow hot wallet to increase max spread * pause funding --- programs/drift/src/instructions/admin.rs | 12 ++++++++++-- programs/drift/src/lib.rs | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 60cf7ca6a1..fc2ee52b74 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -3307,12 +3307,20 @@ pub fn handle_update_perp_market_status( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_paused_operations( - ctx: Context, + ctx: Context, paused_operations: u8, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; msg!("perp market {}", perp_market.market_index); + if *ctx.accounts.admin.key != ctx.accounts.state.admin { + validate!( + paused_operations == PerpOperation::UpdateFunding as u8, + ErrorCode::DefaultError, + "signer must be admin", + )?; + } + perp_market.paused_operations = paused_operations; if perp_market.is_prediction_market() { @@ -3834,7 +3842,7 @@ pub fn handle_update_amm_jit_intensity( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_max_spread( - ctx: Context, + ctx: Context, max_spread: u32, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 09bb1a2d31..432807ec5e 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1420,7 +1420,7 @@ pub mod drift { } pub fn update_perp_market_paused_operations( - ctx: Context, + ctx: Context, paused_operations: u8, ) -> Result<()> { handle_update_perp_market_paused_operations(ctx, paused_operations) @@ -1571,7 +1571,7 @@ pub mod drift { } pub fn update_perp_market_max_spread( - ctx: Context, + ctx: Context, max_spread: u32, ) -> Result<()> { handle_update_perp_market_max_spread(ctx, max_spread) From 08ff4cb1d8a736c17a676e9eb1f034aabc83a80d Mon Sep 17 00:00:00 2001 From: lil perp Date: Tue, 14 Oct 2025 19:57:19 -0400 Subject: [PATCH 128/159] program: allow settling positive pnl expired pos during liquidation (#1959) * program: allow settling positive pnl expired pos during liquidation * fix --- programs/drift/src/controller/pnl.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 11f7f4c40e..7c84c3ae06 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -414,8 +414,20 @@ pub fn settle_expired_position( ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + let position_index = match get_position_index(&user.perp_positions, perp_market_index) { + Ok(index) => index, + Err(_) => { + msg!("User has no position for market {}", perp_market_index); + return Ok(()); + } + }; + + let can_skip_margin_calc = user.perp_positions[position_index].base_asset_amount == 0 + && user.perp_positions[position_index].quote_asset_amount > 0; + // cannot settle pnl this way on a user who is in liquidation territory - if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) + if !meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)? + && !can_skip_margin_calc { return Err(ErrorCode::InsufficientCollateralForSettlingPNL); } @@ -452,14 +464,6 @@ pub fn settle_expired_position( true, )?; - let position_index = match get_position_index(&user.perp_positions, perp_market_index) { - Ok(index) => index, - Err(_) => { - msg!("User has no position for market {}", perp_market_index); - return Ok(()); - } - }; - let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; let perp_market = &mut perp_market_map.get_ref_mut(&perp_market_index)?; validate!( From 11e36803cb0e45d1cbd9eaaddf7756af2f893066 Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 14 Oct 2025 19:52:54 -0700 Subject: [PATCH 129/159] v2.142.0 --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a4c22069..f34fa197f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,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/Cargo.lock b/Cargo.lock index 10989da811..4d0ca24568 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.141.0" +version = "2.142.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 76ff2d9ab3..c69e174701 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.141.0" +version = "2.142.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index f823f96f68..c183e52a45 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.27", + "version": "2.142.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 52e1f4ceed..526f717f78 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.141.0", + "version": "2.142.0", "name": "drift", "instructions": [ { From 7301396172a67eca0deba9d4810efd11d55d0bbd Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 03:01:29 +0000 Subject: [PATCH 130/159] sdk: release v2.143.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 3d464037fe..96ecb56281 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.27 \ No newline at end of file +2.143.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c183e52a45..02280ffb4a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0", + "version": "2.143.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From d6385c484b7ccf6579bd7c363c7156f9b6c58886 Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Wed, 15 Oct 2025 20:29:06 +0800 Subject: [PATCH 131/159] rm println (#1962) --- programs/drift/src/state/perp_market.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 6cca2d4ced..42f750382e 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -762,6 +762,12 @@ impl PerpMarket { let oracle_plus_funding_basis = oracle_price.safe_add(last_funding_basis)?.cast::()?; let median_price = if last_fill_price > 0 { + msg!( + "last_fill_price: {} oracle_plus_funding_basis: {} oracle_plus_basis_5min: {}", + last_fill_price, + oracle_plus_funding_basis, + oracle_plus_basis_5min + ); let mut prices = [ last_fill_price, oracle_plus_funding_basis, From 84128af87f0fb952aa74c9ff8226c7e8b4d23177 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 15 Oct 2025 12:28:27 -0600 Subject: [PATCH 132/159] fix: cleanup potential mem leaks on grpc v2 (#1963) * fix: cleanup potential mem leaks on grpc v2 * fix: lint and prettier * fix: lint again * feat: higher load grpc client test * fix: lint --- ...test.ts => grpc-client-test-comparison.ts} | 0 sdk/scripts/single-grpc-client-test.ts | 226 ++++++++++++++++++ .../grpcDriftClientAccountSubscriberV2.ts | 47 +++- 3 files changed, 269 insertions(+), 4 deletions(-) rename sdk/scripts/{client-test.ts => grpc-client-test-comparison.ts} (100%) create mode 100644 sdk/scripts/single-grpc-client-test.ts diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/grpc-client-test-comparison.ts similarity index 100% rename from sdk/scripts/client-test.ts rename to sdk/scripts/grpc-client-test-comparison.ts diff --git a/sdk/scripts/single-grpc-client-test.ts b/sdk/scripts/single-grpc-client-test.ts new file mode 100644 index 0000000000..0aca8985a0 --- /dev/null +++ b/sdk/scripts/single-grpc-client-test.ts @@ -0,0 +1,226 @@ +import { DriftClient } from '../src/driftClient'; +import { grpcDriftClientAccountSubscriberV2 } from '../src/accounts/grpcDriftClientAccountSubscriberV2'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { DriftClientConfig } from '../src/driftClientConfig'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + Wallet, + OracleInfo, + decodeName, +} from '../src'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import dotenv from 'dotenv'; +import { + AnchorProvider, + Idl, + Program, + ProgramAccount, +} from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; + +const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; +const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + +async function initializeSingleGrpcClient() { + console.log('🚀 Initializing single gRPC Drift Client...'); + + const connection = new Connection(RPC_ENDPOINT); + const wallet = new Wallet(new Keypair()); + dotenv.config({ path: '../' }); + + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const provider = new AnchorProvider( + connection, + // @ts-ignore + wallet, + { + commitment: 'processed', + } + ); + + const program = new Program(driftIDL as Idl, programId, provider); + + // Get perp market accounts + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes( + val.account.marketIndex + ) + ); + const perpMarketIndexes = perpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + // Get spot market accounts + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5].includes(val.account.marketIndex) + ); + const spotMarketIndexes = spotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + // Get oracle infos + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of perpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.amm.oracle, + source: acct.account.amm.oracleSource, + }); + } + } + for (const acct of spotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.oracle, + source: acct.account.oracleSource, + }); + } + } + + console.log(`📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot`); + console.log(`🔮 Oracles: ${oracleInfos.length}`); + + const baseAccountSubscription = { + type: 'grpc' as const, + grpcConfigs: { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: CommitmentLevel.PROCESSED, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }, + }; + + const config: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + ...baseAccountSubscription, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + }; + + const client = new DriftClient(config); + + // Set up event listeners + const eventCounts = { + stateAccountUpdate: 0, + perpMarketAccountUpdate: 0, + spotMarketAccountUpdate: 0, + oraclePriceUpdate: 0, + update: 0, + }; + + console.log('🎧 Setting up event listeners...'); + + client.eventEmitter.on('stateAccountUpdate', (_data) => { + eventCounts.stateAccountUpdate++; + }); + + client.eventEmitter.on('perpMarketAccountUpdate', (_data) => { + eventCounts.perpMarketAccountUpdate++; + }); + + client.eventEmitter.on('spotMarketAccountUpdate', (_data) => { + eventCounts.spotMarketAccountUpdate++; + }); + + client.eventEmitter.on('oraclePriceUpdate', (_publicKey, _source, _data) => { + eventCounts.oraclePriceUpdate++; + }); + + client.accountSubscriber.eventEmitter.on('update', () => { + eventCounts.update++; + }); + + // Subscribe + console.log('🔗 Subscribing to accounts...'); + await client.subscribe(); + + console.log('✅ Client subscribed successfully!'); + console.log('🚀 Starting high-load testing (50 reads/sec per perp market)...'); + + // High-frequency load testing - 50 reads per second per perp market + const loadTestInterval = setInterval(async () => { + try { + // Test getPerpMarketAccount for each perp market (50 times per second per market) + for (const marketIndex of perpMarketIndexes) { + const perpMarketAccount = client.getPerpMarketAccount(marketIndex); + console.log("perpMarketAccount name: ", decodeName(perpMarketAccount.name)); + console.log("perpMarketAccount data: ", JSON.stringify({ + marketIndex: perpMarketAccount.marketIndex, + name: decodeName(perpMarketAccount.name), + baseAssetReserve: perpMarketAccount.amm.baseAssetReserve.toString(), + quoteAssetReserve: perpMarketAccount.amm.quoteAssetReserve.toString() + })); + } + + // Test getMMOracleDataForPerpMarket for each perp market (50 times per second per market) + for (const marketIndex of perpMarketIndexes) { + try { + const oracleData = client.getMMOracleDataForPerpMarket(marketIndex); + console.log("oracleData price: ", oracleData.price.toString()); + console.log("oracleData: ", JSON.stringify({ + price: oracleData.price.toString(), + confidence: oracleData.confidence?.toString(), + slot: oracleData.slot?.toString() + })); + } catch (error) { + // Ignore errors for load testing + } + } + } catch (error) { + console.error('Load test error:', error); + } + }, 20); // 50 times per second = 1000ms / 50 = 20ms interval + + // Log periodic stats + const statsInterval = setInterval(() => { + console.log('\n📈 Event Counts:', eventCounts); + console.log(`⏱️ Client subscribed: ${client.isSubscribed}`); + console.log(`🔗 Account subscriber subscribed: ${client.accountSubscriber.isSubscribed}`); + console.log(`🔥 Load: ${perpMarketIndexes.length * 50 * 2} reads/sec (${perpMarketIndexes.length} markets × 50 getPerpMarketAccount + 50 getMMOracleDataForPerpMarket)`); + }, 5000); + + // Handle shutdown signals - just exit without cleanup since they never unsubscribe + process.on('SIGINT', () => { + console.log('\n🛑 Shutting down...'); + clearInterval(loadTestInterval); + clearInterval(statsInterval); + process.exit(0); + }); + + process.on('SIGTERM', () => { + console.log('\n🛑 Shutting down...'); + clearInterval(loadTestInterval); + clearInterval(statsInterval); + process.exit(0); + }); + + return client; +} + +initializeSingleGrpcClient().catch(console.error); diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 9abfc93c21..ccea364002 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -227,10 +227,11 @@ export class grpcDriftClientAccountSubscriberV2 o.source === oracleInfo.source && o.publicKey.equals(oracleInfo.publicKey) ); - if (!exists) { - this.oracleInfos = this.oracleInfos.concat(oracleInfo); + if (exists) { + return true; // Already exists, don't add duplicate } + this.oracleInfos = this.oracleInfos.concat(oracleInfo); this.oracleMultiSubscriber?.addAccounts([oracleInfo.publicKey]); return true; @@ -708,11 +709,37 @@ export class grpcDriftClientAccountSubscriberV2 await this.perpMarketsSubscriber.removeAccounts( perpMarketPubkeysToRemove ); + // Clean up the mapping for removed perp markets + for (const pubkey of perpMarketPubkeysToRemove) { + const pubkeyString = pubkey.toBase58(); + for (const [ + marketIndex, + accountPubkey, + ] of this.perpMarketIndexToAccountPubkeyMap.entries()) { + if (accountPubkey === pubkeyString) { + this.perpMarketIndexToAccountPubkeyMap.delete(marketIndex); + this.perpOracleMap.delete(marketIndex); + this.perpOracleStringMap.delete(marketIndex); + break; + } + } + } } // Remove accounts in batches - oracles if (oraclePubkeysToRemove.length > 0) { await this.oracleMultiSubscriber.removeAccounts(oraclePubkeysToRemove); + // Clean up oracle data for removed oracles by finding their sources + for (const pubkey of oraclePubkeysToRemove) { + // Find the oracle source by checking oracleInfos + const oracleInfo = this.oracleInfos.find((info) => + info.publicKey.equals(pubkey) + ); + if (oracleInfo) { + const oracleId = getOracleId(pubkey, oracleInfo.source); + this.oracleIdToOracleDataMap.delete(oracleId); + } + } } } @@ -731,13 +758,25 @@ export class grpcDriftClientAccountSubscriberV2 } async unsubscribe(): Promise { - if (this.isSubscribed) { + if (!this.isSubscribed) { return; } - await this.stateAccountSubscriber.unsubscribe(); + this.isSubscribed = false; + this.isSubscribing = false; + + await this.stateAccountSubscriber?.unsubscribe(); await this.unsubscribeFromOracles(); await this.perpMarketsSubscriber?.unsubscribe(); await this.spotMarketsSubscriber?.unsubscribe(); + + // Clean up all maps to prevent memory leaks + this.perpMarketIndexToAccountPubkeyMap.clear(); + this.spotMarketIndexToAccountPubkeyMap.clear(); + this.oracleIdToOracleDataMap.clear(); + this.perpOracleMap.clear(); + this.perpOracleStringMap.clear(); + this.spotOracleMap.clear(); + this.spotOracleStringMap.clear(); } } From b767e186aed21fb061a353fe5e00183c17289763 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:34:01 +0000 Subject: [PATCH 133/159] sdk: release v2.143.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 96ecb56281..b807294192 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.0 \ No newline at end of file +2.143.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 02280ffb4a..9324ff0ae6 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.0", + "version": "2.143.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From d4ad8db5fcf04fe03e6cfdbfac42cd114134d15c Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Thu, 16 Oct 2025 09:57:26 -0700 Subject: [PATCH 134/159] Moose review (#1948) * better cap fees * add more constraints for user token accounts * use oracle map for updating aum * cargo tests pass and adding oracle map usage to derivative constituent in aum target as well --- programs/drift/src/instructions/lp_admin.rs | 1 - programs/drift/src/instructions/lp_pool.rs | 30 +- programs/drift/src/state/lp_pool.rs | 108 +++++- programs/drift/src/state/lp_pool/tests.rs | 369 ++++++++++++++++++-- sdk/src/driftClient.ts | 92 ++--- 5 files changed, 481 insertions(+), 119 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 5c406501fe..2d53cc71e2 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1079,7 +1079,6 @@ pub struct InitializeLpPool<'info> { spot_market_index: u16, )] pub struct InitializeConstituent<'info> { - #[account()] pub state: Box>, #[account( mut, diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 42a0828037..5699598636 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -3,7 +3,6 @@ use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::ids::lp_pool_swap_wallet; use crate::math::constants::PRICE_PRECISION_I64; -use crate::math::oracle::OracleValidity; use crate::state::events::{DepositDirection, LPBorrowLendDepositRecord}; use crate::state::paused_operations::ConstituentLpOperation; use crate::validation::whitelist::validate_whitelist_token; @@ -32,7 +31,6 @@ use crate::{ update_constituent_target_base_for_derivatives, AmmConstituentDatum, AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, ConstituentTargetBaseFixed, LPPool, TargetsDatum, LP_POOL_SWAP_AUM_UPDATE_DELAY, - MAX_ORACLE_STALENESS_FOR_TARGET_CALC, MAX_STALENESS_FOR_TARGET_CALC, }, oracle_map::OracleMap, perp_market_map::MarketSet, @@ -45,7 +43,6 @@ use crate::{ }, validate, }; -use std::convert::TryFrom; use std::iter::Peekable; use std::slice::Iter; @@ -164,7 +161,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( let AccountMaps { perp_market_map: _, spot_market_map, - oracle_map: _, + mut oracle_map, } = load_maps( remaining_accounts, &MarketSet::new(), @@ -202,6 +199,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( slot, &constituent_map, &spot_market_map, + &mut oracle_map, &constituent_target_base, &amm_cache, )?; @@ -227,6 +225,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, )?; @@ -1316,8 +1315,6 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( )?; let out_oracle = out_oracle.clone(); - // TODO: check self.aum validity - if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { msg!( "Out oracle data for spot market {} is invalid for lp pool swap.", @@ -1330,7 +1327,7 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( out_constituent.constituent_index, &out_spot_market, out_oracle.price, - lp_pool.last_aum, // TODO: remove out_amount * out_oracle to est post remove_liquidity aum + lp_pool.last_aum, )?; let dlp_total_supply = ctx.accounts.lp_mint.supply; @@ -1410,7 +1407,6 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( let deposit_plus_token_amount_before = amount.safe_add(spot_market_vault.amount)?; let oracle_data = oracle_map.get_price_data(&oracle_id)?; - let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; controller::spot_balance::update_spot_market_cumulative_interest( &mut spot_market, @@ -1425,10 +1421,6 @@ pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( .cast::()? .safe_sub(constituent.last_spot_balance_token_amount)?; - if constituent.last_oracle_slot < oracle_data_slot { - constituent.last_oracle_price = oracle_data.price; - constituent.last_oracle_slot = oracle_data_slot; - } constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); let balance_before = constituent.get_full_token_amount(&spot_market)?; @@ -1559,11 +1551,6 @@ pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( .cast::()? .safe_sub(constituent.last_spot_balance_token_amount)?; - if constituent.last_oracle_slot < oracle_data_slot { - constituent.last_oracle_price = oracle_data.price; - constituent.last_oracle_slot = oracle_data_slot; - } - let mint = &Some(*ctx.accounts.mint.clone()); transfer_from_program_vault( amount, @@ -1865,12 +1852,12 @@ pub struct LPPoolSwap<'info> { #[account( mut, - constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() )] pub user_in_token_account: Box>, #[account( mut, - constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() )] pub user_out_token_account: Box>, @@ -1900,7 +1887,6 @@ pub struct LPPoolSwap<'info> { pub authority: Signer<'info>, - // TODO: in/out token program pub token_program: Interface<'info, TokenInterface>, } @@ -1974,7 +1960,7 @@ pub struct LPPoolAddLiquidity<'info> { #[account( mut, - constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() )] pub user_in_token_account: Box>, @@ -2058,7 +2044,7 @@ pub struct LPPoolRemoveLiquidity<'info> { #[account( mut, - constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() )] pub user_out_token_account: Box>, #[account( diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 667e8da54d..033d67c780 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::f32::consts::E; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; @@ -6,13 +7,16 @@ use crate::math::constants::{ BASE_PRECISION_I128, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, }; +use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::math::spot_balance::{get_signed_token_amount, get_token_amount}; use crate::state::amm_cache::{AmmCacheFixed, CacheInfo}; use crate::state::constituent_map::ConstituentMap; +use crate::state::oracle_map::OracleMap; use crate::state::paused_operations::ConstituentLpOperation; use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::MarketType; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use enumflags2::BitFlags; @@ -32,9 +36,8 @@ pub const CONSTITUENT_CORRELATIONS_PDA_SEED: &str = "constituent_correlations"; pub const CONSTITUENT_VAULT_PDA_SEED: &str = "CONSTITUENT_VAULT"; pub const LP_POOL_TOKEN_VAULT_PDA_SEED: &str = "LP_POOL_TOKEN_VAULT"; -pub const BASE_SWAP_FEE: i128 = 300; // 0.75% in PERCENTAGE_PRECISION -pub const MAX_SWAP_FEE: i128 = 75_000; // 0.75% in PERCENTAGE_PRECISION -pub const MIN_SWAP_FEE: i128 = 200; // 0.75% in PERCENTAGE_PRECISION +pub const BASE_SWAP_FEE: i128 = 300; // 0.3% in PERCENTAGE_PRECISION +pub const MAX_SWAP_FEE: i128 = 37_500; // 37.5% in PERCENTAGE_PRECISION pub const MIN_AUM_EXECUTION_FEE: u128 = 10_000_000_000_000; @@ -225,6 +228,9 @@ impl LPPool { out_target_oracle_slot_delay, )?; + in_fee = in_fee.min(MAX_SWAP_FEE); + out_fee = out_fee.min(MAX_SWAP_FEE); + let in_fee_amount = in_amount .cast::()? .safe_mul(in_fee)? @@ -279,6 +285,7 @@ impl LPPool { in_target_position_slot_delay, in_target_oracle_slot_delay, )?; + in_fee_pct = in_fee_pct.min(MAX_SWAP_FEE * 2); let in_fee_amount = in_amount .cast::()? @@ -383,6 +390,8 @@ impl LPPool { out_target_oracle_slot_delay, )?; out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + out_fee_pct = out_fee_pct.min(MAX_SWAP_FEE * 2); + let out_fee_amount = out_amount .cast::()? .safe_mul(out_fee_pct)? @@ -629,10 +638,7 @@ impl LPPool { .safe_add(out_quadratic_inventory_fee)? .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; - Ok(( - total_in_fee.min(MAX_SWAP_FEE.safe_div(2)?), - total_out_fee.min(MAX_SWAP_FEE.safe_div(2)?), - )) + Ok((total_in_fee, total_out_fee)) } pub fn get_target_uncertainty_fees( @@ -686,6 +692,7 @@ impl LPPool { slot: u64, constituent_map: &ConstituentMap, spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, constituent_target_base: &AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, amm_cache: &AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed>, ) -> DriftResult<(u128, i128, BTreeMap>)> { @@ -723,10 +730,28 @@ impl LPPool { } let spot_market = spot_market_map.get_ref(&constituent.spot_market_index)?; + let oracle_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &spot_market.oracle_id(), + spot_market.historical_oracle_data.last_oracle_price_twap, + spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + if !is_oracle_valid_for_action( + oracle_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is not valid for action", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle.into()); + } let constituent_aum = constituent .get_full_token_amount(&spot_market)? - .safe_mul(constituent.last_oracle_price as i128)? + .safe_mul(oracle_and_validity.0.price as i128)? .safe_div(10_i128.pow(spot_market.decimals))?; msg!( "constituent: {}, balance: {}, aum: {}, deriv index: {}, bl token balance {}, bl balance type {}, vault balance: {}", @@ -747,7 +772,7 @@ impl LPPool { .get(constituent.constituent_index as u32) .target_base .cast::()? - .safe_mul(constituent.last_oracle_price.cast::()?)? + .safe_mul(oracle_and_validity.0.price.cast::()?)? .safe_div(10_i128.pow(constituent.decimals as u32))? .cast::()?; crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?; @@ -1579,8 +1604,8 @@ impl ConstituentCorrelations { "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" )?; - self.correlations[(i as usize * num_constituents + j as usize)] = corr; - self.correlations[(j as usize * num_constituents + i as usize)] = corr; + self.correlations[i as usize * num_constituents + j as usize] = corr; + self.correlations[j as usize * num_constituents + i as usize] = corr; self.validate()?; @@ -1662,34 +1687,81 @@ pub fn update_constituent_target_base_for_derivatives( derivative_groups: &BTreeMap>, constituent_map: &ConstituentMap, spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, constituent_target_base: &mut AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, ) -> DriftResult<()> { for (parent_index, constituent_indexes) in derivative_groups.iter() { let parent_constituent = constituent_map.get_ref(parent_index)?; + + let parent_spot_market = spot_market_map.get_ref(&parent_constituent.spot_market_index)?; + let parent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + parent_spot_market.market_index, + &parent_spot_market.oracle_id(), + parent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + parent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + if !is_oracle_valid_for_action( + parent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Parent constituent {} oracle is invalid", + parent_constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + let parent_constituent_price = parent_oracle_price_and_validity.0.price; + let parent_target_base = constituent_target_base .get(*parent_index as u32) .target_base; let target_parent_weight = calculate_target_weight( parent_target_base, &*spot_market_map.get_ref(&parent_constituent.spot_market_index)?, - parent_constituent.last_oracle_price, + parent_oracle_price_and_validity.0.price, aum, )?; let mut derivative_weights_sum: u64 = 0; for constituent_index in constituent_indexes { let constituent = constituent_map.get_ref(constituent_index)?; - if constituent.last_oracle_price - < parent_constituent - .last_oracle_price + let constituent_spot_market = + spot_market_map.get_ref(&constituent.spot_market_index)?; + let constituent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &constituent_spot_market.oracle_id(), + constituent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + constituent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + if !is_oracle_valid_for_action( + constituent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is invalid", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + + if constituent_oracle_price_and_validity.0.price + < parent_constituent_price .safe_mul(constituent.constituent_derivative_depeg_threshold as i64)? .safe_div(PERCENTAGE_PRECISION_I64)? { msg!( "Constituent {} last oracle price {} is too low compared to parent constituent {} last oracle price {}. Assuming depegging and setting target base to 0.", constituent.constituent_index, - constituent.last_oracle_price, + constituent_oracle_price_and_validity.0.price, parent_constituent.constituent_index, - parent_constituent.last_oracle_price + parent_constituent_price ); constituent_target_base .get_mut(*constituent_index as u32) @@ -1714,7 +1786,7 @@ pub fn update_constituent_target_base_for_derivatives( .safe_mul(target_weight)? .safe_div(PERCENTAGE_PRECISION_I128)? .safe_mul(10_i128.pow(constituent.decimals as u32))? - .safe_div(constituent.last_oracle_price as i128)?; + .safe_div(constituent_oracle_price_and_validity.0.price as i128)?; msg!( "constituent: {}, target base: {}", diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index a460bcd151..19c15b45be 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -273,7 +273,6 @@ mod tests { price, }]; let aum = 1_000_000; - let now_ts = 1234; let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 1, @@ -384,7 +383,6 @@ mod tests { ]; let aum = 1_000_000; - let now_ts = 999; let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: amm_mapping_data.len() as u32, @@ -469,8 +467,6 @@ mod tests { price: 142_000_000, }]; - let now_ts = 111; - let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 1, ..ConstituentTargetBaseFixed::default() @@ -499,8 +495,6 @@ mod tests { #[cfg(test)] mod swap_tests { - use core::slice; - use crate::math::constants::{ PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I128, PRICE_PRECISION_I64, SPOT_BALANCE_PRECISION, @@ -774,7 +768,7 @@ mod swap_tests { in_target_position_delay: u64, in_target_oracle_delay: u64, last_aum: u128, - now: i64, + _now: i64, in_decimals: u32, in_amount: u128, dlp_total_supply: u64, @@ -964,9 +958,9 @@ mod swap_tests { fn get_remove_liquidity_mint_amount_scenario( out_target_position_delay: u64, - out_target_oracle_delay: u64, + _out_target_oracle_delay: u64, last_aum: u128, - now: i64, + _now: i64, in_decimals: u32, lp_burn_amount: u64, dlp_total_supply: u64, @@ -2378,6 +2372,8 @@ mod update_aum_tests { state::lp_pool::*, state::oracle::HistoricalOracleData, state::oracle::OracleSource, + state::oracle_map::OracleMap, + state::pyth_lazer_oracle::PythLazerOracle, state::spot_market::SpotMarket, state::spot_market_map::SpotMarketMap, state::zero_copy::AccountZeroCopyMut, @@ -2462,6 +2458,56 @@ mod update_aum_tests { ) .unwrap(); + // Create simple PythLazer oracle accounts for non-quote assets with prices matching constituents + // Use exponent -6 so values are already in PRICE_PRECISION units + let sol_oracle_pubkey = Pubkey::new_unique(); + let mut sol_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + sol_oracle, + &sol_oracle_pubkey, + PythLazerOracle, + sol_oracle_account_info + ); + + let btc_oracle_pubkey = Pubkey::new_unique(); + let mut btc_oracle = PythLazerOracle { + price: 100_000 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + btc_oracle, + &btc_oracle_pubkey, + PythLazerOracle, + btc_oracle_account_info + ); + + let bonk_oracle_pubkey = Pubkey::new_unique(); + let mut bonk_oracle = PythLazerOracle { + price: 22, // $0.000022 in PRICE_PRECISION + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + bonk_oracle, + &bonk_oracle_pubkey, + PythLazerOracle, + bonk_oracle_account_info + ); + // Create spot markets let mut usdc_spot_market = SpotMarket { market_index: 0, @@ -2475,30 +2521,35 @@ mod update_aum_tests { let mut sol_spot_market = SpotMarket { market_index: 1, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: sol_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(200 * PRICE_PRECISION_I64), ..SpotMarket::default() }; create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); let mut btc_spot_market = SpotMarket { market_index: 2, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: btc_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 8, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price( + 100_000 * PRICE_PRECISION_I64, + ), ..SpotMarket::default() }; create_anchor_account_info!(btc_spot_market, SpotMarket, btc_spot_market_account_info); let mut bonk_spot_market = SpotMarket { market_index: 3, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: bonk_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 5, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(22), ..SpotMarket::default() }; create_anchor_account_info!(bonk_spot_market, SpotMarket, bonk_spot_market_account_info); @@ -2511,6 +2562,21 @@ mod update_aum_tests { ]; let spot_market_map = SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + // Build an oracle map containing the three non-quote oracles + let oracle_accounts = vec![ + sol_oracle_account_info.clone(), + btc_oracle_account_info.clone(), + bonk_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + msg!( + "oracle map entry 0 {:?}", + oracle_map + .get_price_data(&sol_spot_market.oracle_id()) + .unwrap() + ); // Create constituent target base let target_fixed = RefCell::new(ConstituentTargetBaseFixed { @@ -2518,7 +2584,7 @@ mod update_aum_tests { ..ConstituentTargetBaseFixed::default() }); let target_data = RefCell::new([0u8; 128]); // 4 * 32 bytes per TargetsDatum - let mut constituent_target_base = + let constituent_target_base = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { fixed: target_fixed.borrow_mut(), data: target_data.borrow_mut(), @@ -2541,6 +2607,7 @@ mod update_aum_tests { 101, // slot &constituent_map, &spot_market_map, + &mut oracle_map, &constituent_target_base, &amm_cache, ); @@ -2677,6 +2744,8 @@ mod update_constituent_target_base_for_derivatives_tests { use crate::state::constituent_map::ConstituentMap; use crate::state::lp_pool::{Constituent, ConstituentTargetBaseFixed, TargetsDatum}; use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::pyth_lazer_oracle::PythLazerOracle; use crate::state::spot_market::SpotMarket; use crate::state::spot_market_map::SpotMarketMap; use crate::state::zero_copy::AccountZeroCopyMut; @@ -2781,13 +2850,79 @@ mod update_constituent_target_base_for_derivatives_tests { ]; let constituent_map = ConstituentMap::load_multiple(constituents_list, true).unwrap(); - // Create spot markets + // Create oracles for parent and derivatives, with prices matching their last_oracle_price + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 205 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + + let derivative3_oracle_pubkey = Pubkey::new_unique(); + let mut derivative3_oracle = PythLazerOracle { + price: 210 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative3_oracle, + &derivative3_oracle_pubkey, + PythLazerOracle, + derivative3_oracle_account_info + ); + + // Create spot markets bound to the above oracles let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2798,10 +2933,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative1_spot_market = SpotMarket { market_index: derivative1_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2812,10 +2948,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative2_spot_market = SpotMarket { market_index: derivative2_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2826,10 +2963,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative3_spot_market = SpotMarket { market_index: derivative3_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative3_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, - historical_oracle_data: HistoricalOracleData::default(), + historical_oracle_data: HistoricalOracleData::default_price(derivative3_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -2846,6 +2984,16 @@ mod update_constituent_target_base_for_derivatives_tests { ]; let spot_market_map = SpotMarketMap::load_multiple(spot_market_list, true).unwrap(); + // Build an oracle map for parent and derivatives + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + derivative3_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + // Create constituent target base let num_constituents = 4; // Fixed: parent + 3 derivatives let target_fixed = RefCell::new(ConstituentTargetBaseFixed { @@ -2913,6 +3061,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); @@ -3049,12 +3198,47 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Create PythLazer oracles corresponding to prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + // Create spot markets let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3065,9 +3249,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative_spot_market = SpotMarket { market_index: derivative_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3085,6 +3271,14 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + // Create constituent target base let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 2, @@ -3116,6 +3310,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); @@ -3292,11 +3487,47 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Create PythLazer oracles so update_constituent_target_base_for_derivatives can fetch current prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + + // Spot markets bound to the test oracles let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3307,9 +3538,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative_spot_market = SpotMarket { market_index: derivative_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3327,6 +3560,14 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 2, ..ConstituentTargetBaseFixed::default() @@ -3355,6 +3596,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); @@ -3446,11 +3688,60 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Oracles + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 198 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + let mut parent_spot_market = SpotMarket { market_index: parent_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3461,9 +3752,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative1_spot_market = SpotMarket { market_index: derivative1_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3474,9 +3767,11 @@ mod update_constituent_target_base_for_derivatives_tests { let mut derivative2_spot_market = SpotMarket { market_index: derivative2_index, - oracle_source: OracleSource::Pyth, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), ..SpotMarket::default() }; create_anchor_account_info!( @@ -3495,6 +3790,15 @@ mod update_constituent_target_base_for_derivatives_tests { ) .unwrap(); + // Oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { len: 3, ..ConstituentTargetBaseFixed::default() @@ -3525,6 +3829,7 @@ mod update_constituent_target_base_for_derivatives_tests { &derivative_groups, &constituent_map, &spot_market_map, + &mut oracle_map, &mut constituent_target_base, ); diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 5afe5a02b3..22559c5d11 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -362,8 +362,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') { @@ -848,8 +848,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) { @@ -1001,7 +1001,7 @@ export class DriftClient { [...this.authoritySubAccountMap.values()][0][0] ?? 0, new PublicKey( [...this.authoritySubAccountMap.keys()][0] ?? - this.authority.toString() + this.authority.toString() ) ); } @@ -3320,19 +3320,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 ( @@ -4557,14 +4557,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; @@ -4712,10 +4712,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({ @@ -5352,14 +5352,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) { @@ -5575,14 +5575,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) { @@ -6921,8 +6921,8 @@ export class DriftClient { makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo - ? [makerInfo] - : []; + ? [makerInfo] + : []; const userAccounts = [this.getUserAccount(subAccountId)]; for (const maker of makerInfo) { @@ -7168,13 +7168,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; } @@ -10893,8 +10893,8 @@ export class DriftClient { ): Promise { const remainingAccounts = userAccount ? this.getRemainingAccounts({ - userAccounts: [userAccount], - }) + userAccounts: [userAccount], + }) : undefined; const ix = await this.program.instruction.disableUserHighLeverageMode( From 5702b5e762cccba8f973908d54450fdd8fdca66b Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:56:01 -0700 Subject: [PATCH 135/159] remove unnecessary admin func --- programs/drift/src/instructions/lp_admin.rs | 25 +++++++++---------- programs/drift/src/lib.rs | 7 ------ sdk/src/adminClient.ts | 27 +++------------------ 3 files changed, 14 insertions(+), 45 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 2d53cc71e2..bc5910dae8 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -113,20 +113,6 @@ pub fn handle_initialize_lp_pool( Ok(()) } -pub fn handle_increase_lp_pool_max_aum( - ctx: Context, - new_max_aum: u128, -) -> Result<()> { - let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; - msg!( - "lp pool max aum: {:?} -> {:?}", - lp_pool.max_aum, - new_max_aum - ); - lp_pool.max_aum = new_max_aum; - Ok(()) -} - pub fn handle_initialize_constituent<'info>( ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, spot_market_index: u16, @@ -399,6 +385,7 @@ pub struct LpPoolParams { pub volatility: Option, pub gamma_execution: Option, pub xi: Option, + pub max_aum: Option, pub whitelist_mint: Option, } @@ -445,6 +432,16 @@ pub fn handle_update_lp_pool_params<'info>( lp_pool.whitelist_mint = whitelist_mint; } + if let Some(max_aum) = lp_pool_params.max_aum { + validate!( + max_aum >= lp_pool.max_aum, + ErrorCode::DefaultError, + "new max_aum must be greater than or equal to current max_aum" + )?; + msg!("max_aum: {:?} -> {:?}", lp_pool.max_aum, max_aum); + lp_pool.max_aum = max_aum; + } + Ok(()) } diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 432807ec5e..cd6f1c45fe 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1851,13 +1851,6 @@ pub mod drift { ) } - pub fn increase_lp_pool_max_aum( - ctx: Context, - new_max_aum: u128, - ) -> Result<()> { - handle_increase_lp_pool_max_aum(ctx, new_max_aum) - } - pub fn update_high_leverage_mode_config( ctx: Context, max_users: u32, diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index fcfd6b1a7e..835f8823ea 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -5323,30 +5323,6 @@ export class AdminClient extends DriftClient { ]; } - public async increaseLpPoolMaxAum( - name: string, - newMaxAum: BN - ): Promise { - const ixs = await this.getIncreaseLpPoolMaxAumIx(name, newMaxAum); - const tx = await this.buildTransaction(ixs); - const { txSig } = await this.sendTransaction(tx, []); - return txSig; - } - - public async getIncreaseLpPoolMaxAumIx( - name: string, - newMaxAum: BN - ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, encodeName(name)); - return this.program.instruction.increaseLpPoolMaxAum(newMaxAum, { - accounts: { - admin: this.wallet.publicKey, - lpPool, - state: await this.getStatePublicKey(), - }, - }); - } - public async initializeConstituent( lpPoolName: number[], initializeConstituentParams: InitializeConstituentParams @@ -5597,6 +5573,7 @@ export class AdminClient extends DriftClient { gammaExecution?: number; xi?: number; whitelistMint?: PublicKey; + maxAum?: BN; } ): Promise { const ixs = await this.getUpdateLpPoolParamsIx( @@ -5616,6 +5593,7 @@ export class AdminClient extends DriftClient { gammaExecution?: number; xi?: number; whitelistMint?: PublicKey; + maxAum?: BN; } ): Promise { const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); @@ -5628,6 +5606,7 @@ export class AdminClient extends DriftClient { gammaExecution: null, xi: null, whitelistMint: null, + maxAum: null, }, updateLpPoolParams ), From f3bd586529de4f36558857d99ecb8822f6df2d13 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:53:47 -0700 Subject: [PATCH 136/159] idl --- sdk/src/idl/drift.json | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 6949be43c1..9d6ac82641 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7372,32 +7372,6 @@ } ] }, - { - "name": "increaseLpPoolMaxAum", - "accounts": [ - { - "name": "lpPool", - "isMut": true, - "isSigner": false - }, - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "state", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "newMaxAum", - "type": "u128" - } - ] - }, { "name": "updateHighLeverageModeConfig", "accounts": [ @@ -12099,6 +12073,12 @@ "option": "u8" } }, + { + "name": "maxAum", + "type": { + "option": "u128" + } + }, { "name": "whitelistMint", "type": { From 9d7e7a07bbb82c65b314187723296ee87036ae37 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 22 Oct 2025 08:51:27 -0700 Subject: [PATCH 137/159] delete unused errors --- programs/drift/src/error.rs | 6 --- sdk/src/driftClient.ts | 92 ++++++++++++++++++------------------ sdk/src/driftClientConfig.ts | 80 +++++++++++++++---------------- tests/lpPool.ts | 13 +++-- tests/lpPoolSwap.ts | 3 +- 5 files changed, 96 insertions(+), 98 deletions(-) diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index eaa8009e65..f95399b190 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -658,8 +658,6 @@ pub enum ErrorCode { InvalidConstituent, #[msg("Invalid Amm Constituent Mapping argument")] InvalidAmmConstituentMappingArgument, - #[msg("Invalid update constituent update target weights argument")] - InvalidUpdateConstituentTargetBaseArgument, #[msg("Constituent not found")] ConstituentNotFound, #[msg("Constituent could not load")] @@ -668,8 +666,6 @@ pub enum ErrorCode { ConstituentWrongMutability, #[msg("Wrong number of constituents passed to instruction")] WrongNumberOfConstituents, - #[msg("Oracle too stale for LP AUM update")] - OracleTooStaleForLPAUMUpdate, #[msg("Insufficient constituent token balance")] InsufficientConstituentTokenBalance, #[msg("Amm Cache data too stale")] @@ -682,8 +678,6 @@ pub enum ErrorCode { LpInvariantFailed, #[msg("Invalid constituent derivative weights")] InvalidConstituentDerivativeWeights, - #[msg("Unauthorized dlp authority")] - UnauthorizedDlpAuthority, #[msg("Max DLP AUM Breached")] MaxDlpAumBreached, #[msg("Settle Lp Pool Disabled")] diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index f3e023ad48..37ae130359 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -362,8 +362,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') { @@ -848,8 +848,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) { @@ -1001,7 +1001,7 @@ export class DriftClient { [...this.authoritySubAccountMap.values()][0][0] ?? 0, new PublicKey( [...this.authoritySubAccountMap.keys()][0] ?? - this.authority.toString() + this.authority.toString() ) ); } @@ -3311,19 +3311,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 ( @@ -4363,14 +4363,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; @@ -4518,10 +4518,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({ @@ -5158,14 +5158,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) { @@ -5381,14 +5381,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) { @@ -6727,8 +6727,8 @@ export class DriftClient { makerInfo = Array.isArray(makerInfo) ? makerInfo : makerInfo - ? [makerInfo] - : []; + ? [makerInfo] + : []; const userAccounts = [this.getUserAccount(subAccountId)]; for (const maker of makerInfo) { @@ -6974,13 +6974,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; } @@ -10699,8 +10699,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 4cb522fc8c..8f479dce81 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -58,45 +58,45 @@ 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; - } + 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; + } | { - 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; - oracleAccountSubscriber?: new ( - accountName: string, - program: Program, - accountPublicKey: PublicKey, - decodeBuffer?: (buffer: Buffer) => any, - resubOpts?: ResubOpts, - commitment?: Commitment - ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - } + 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; + oracleAccountSubscriber?: new ( + accountName: string, + 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/tests/lpPool.ts b/tests/lpPool.ts index 330ede5f6b..c0b6867659 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -614,7 +614,8 @@ describe('LP Pool', () => { }); expect.fail('should have failed'); } catch (e) { - assert(e.message.includes('0x18bf')); // LpPoolAumDelayed + console.log(e.message); + assert(e.message.includes('0x18bd')); // LpPoolAumDelayed } }); @@ -640,7 +641,7 @@ describe('LP Pool', () => { await adminClient.sendTransaction(tx); } catch (e) { console.log(e.message); - assert(e.message.includes('0x18c8')); // InvalidConstituentOperation + assert(e.message.includes('0x18c5')); // InvalidConstituentOperation } await adminClient.updateConstituentPausedOperations( getConstituentPublicKey(program.programId, lpPoolKey, 0), @@ -699,7 +700,8 @@ describe('LP Pool', () => { await adminClient.updateLpPoolAum(lpPool, [0]); expect.fail('should have failed'); } catch (e) { - assert(e.message.includes('0x18bb')); // WrongNumberOfConstituents + console.log(e.message); + assert(e.message.includes('0x18ba')); // WrongNumberOfConstituents } }); @@ -1584,7 +1586,7 @@ describe('LP Pool', () => { ); } catch (e) { console.log(e); - assert(e.toString().includes('0x18c1')); // invariant failed + assert(e.toString().includes('0x18bf')); // invariant failed } }); @@ -1595,7 +1597,8 @@ describe('LP Pool', () => { await adminClient.settlePerpToLpPool(encodeName(lpPoolName), [0, 1, 2]); assert(false, 'Should have thrown'); } catch (e) { - assert(e.message.includes('0x18c5')); // SettleLpPoolDisabled + console.log(e.message); + assert(e.message.includes('0x18c2')); // SettleLpPoolDisabled } await adminClient.updateFeatureBitFlagsSettleLpPool(true); diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 764dd941ca..4c2aa62cfc 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -589,7 +589,8 @@ describe('LP Pool', () => { try { await adminClient.sendTransaction(tx); } catch (e) { - assert(e.message.includes('0x18c8')); // InvalidConstituentOperation + console.log(e.message); + assert(e.message.includes('0x18c5')); // InvalidConstituentOperation } await adminClient.updateConstituentStatus( c0.pubkey, From 309fa74130c6ba958a9dc1ff41a92089aaadbcfc Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 22 Oct 2025 08:52:17 -0700 Subject: [PATCH 138/159] make linter happy --- sdk/src/driftClientConfig.ts | 83 +++++++++++++++++------------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index 8f479dce81..b1a867a851 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -19,9 +19,6 @@ import { import { Coder, Program } from '@coral-xyz/anchor'; import { WebSocketAccountSubscriber } from './accounts/webSocketAccountSubscriber'; import { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscriberV2'; -import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; -import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; -import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; import { grpcDriftClientAccountSubscriberV2 } from './accounts/grpcDriftClientAccountSubscriberV2'; import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; @@ -58,45 +55,45 @@ 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; - } + 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; + } | { - 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; - oracleAccountSubscriber?: new ( - accountName: string, - program: Program, - accountPublicKey: PublicKey, - decodeBuffer?: (buffer: Buffer) => any, - resubOpts?: ResubOpts, - commitment?: Commitment - ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - } + 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; + oracleAccountSubscriber?: new ( + accountName: string, + program: Program, + accountPublicKey: PublicKey, + decodeBuffer?: (buffer: Buffer) => any, + resubOpts?: ResubOpts, + commitment?: Commitment + ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; + } | { - type: 'polling'; - accountLoader: BulkAccountLoader; - }; + type: 'polling'; + accountLoader: BulkAccountLoader; + }; From 648bf339a20d7417d44c44ad2cd838668253f895 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:53:50 -0700 Subject: [PATCH 139/159] fix all compiler warnings --- programs/drift/src/controller/insurance.rs | 6 +- programs/drift/src/controller/liquidation.rs | 10 ++- programs/drift/src/controller/orders.rs | 6 +- .../src/controller/orders/amm_lp_jit_tests.rs | 6 +- .../drift/src/controller/position/tests.rs | 47 ++++------- programs/drift/src/controller/repeg/tests.rs | 4 +- .../src/controller/spot_balance/tests.rs | 4 +- programs/drift/src/ids.rs | 3 - programs/drift/src/instructions/admin.rs | 21 ++--- programs/drift/src/instructions/if_staker.rs | 3 +- programs/drift/src/instructions/keeper.rs | 13 ++- programs/drift/src/instructions/lp_admin.rs | 1 - programs/drift/src/instructions/lp_pool.rs | 1 - programs/drift/src/instructions/user.rs | 5 +- programs/drift/src/lib.rs | 5 +- programs/drift/src/math/amm.rs | 6 +- programs/drift/src/math/auction.rs | 4 +- programs/drift/src/math/cp_curve/tests.rs | 10 +-- programs/drift/src/math/fees.rs | 2 +- programs/drift/src/math/fuel.rs | 2 +- programs/drift/src/math/insurance.rs | 3 +- programs/drift/src/math/liquidation.rs | 6 +- programs/drift/src/math/lp_pool.rs | 1 + programs/drift/src/math/margin.rs | 6 +- programs/drift/src/math/margin/tests.rs | 5 +- programs/drift/src/math/orders.rs | 15 ++-- programs/drift/src/math/orders/tests.rs | 2 +- programs/drift/src/math/position.rs | 5 +- programs/drift/src/math/spot_swap.rs | 3 +- .../drift/src/state/insurance_fund_stake.rs | 3 +- programs/drift/src/state/lp_pool.rs | 1 - programs/drift/src/state/lp_pool/tests.rs | 9 +-- .../drift/src/state/margin_calculation.rs | 7 +- programs/drift/src/state/oracle.rs | 1 - programs/drift/src/state/order_params.rs | 8 +- programs/drift/src/state/perp_market.rs | 4 +- programs/drift/src/state/spot_market.rs | 5 +- programs/drift/src/state/state.rs | 4 +- programs/drift/src/state/user.rs | 13 +-- programs/drift/src/state/user/tests.rs | 4 +- programs/drift/src/validation/margin.rs | 6 +- programs/drift/src/validation/order.rs | 3 +- programs/drift/src/validation/perp_market.rs | 8 +- programs/drift/src/validation/user.rs | 3 +- sdk/src/driftClientConfig.ts | 80 +++++++++---------- 45 files changed, 164 insertions(+), 200 deletions(-) diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 17cb020405..fa53164733 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -14,9 +14,9 @@ use crate::error::ErrorCode; use crate::math::amm::calculate_net_user_pnl; use crate::math::casting::Cast; use crate::math::constants::{ - MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, + FUEL_START_TS, GOV_SPOT_MARKET_INDEX, MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT_GOV, ONE_YEAR, PERCENTAGE_PRECISION, - SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_DENOMINATOR, + QUOTE_SPOT_MARKET_INDEX, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_DENOMINATOR, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_NUMERATOR, }; use crate::math::fuel::calculate_insurance_fuel_bonus; @@ -40,7 +40,7 @@ use crate::state::perp_market::PerpMarket; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::state::State; use crate::state::user::UserStats; -use crate::{emit, validate, FUEL_START_TS, GOV_SPOT_MARKET_INDEX, QUOTE_SPOT_MARKET_INDEX}; +use crate::{emit, validate}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 9503f24966..4e5543d313 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -21,8 +21,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::bankruptcy::is_user_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ - LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, QUOTE_PRECISION, - QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, SPOT_WEIGHT_PRECISION, + LIQUIDATION_FEE_PRECISION, LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, + QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, + SPOT_WEIGHT_PRECISION, }; use crate::math::liquidation::{ calculate_asset_transfer_for_liability_transfer, @@ -48,6 +49,7 @@ use crate::math::orders::{ use crate::math::position::calculate_base_asset_value_with_oracle_price; use crate::math::safe_math::SafeMath; +use crate::math::constants::LST_POOL_ID; use crate::math::spot_balance::get_token_value; use crate::state::events::{ LiquidateBorrowForPerpPnlRecord, LiquidatePerpPnlForDepositRecord, LiquidatePerpRecord, @@ -66,8 +68,8 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User, UserStats}; use crate::state::user_map::{UserMap, UserStatsMap}; -use crate::{get_then_update_id, load_mut, LST_POOL_ID}; -use crate::{validate, LIQUIDATION_FEE_PRECISION}; +use crate::validate; +use crate::{get_then_update_id, load_mut}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6d4533dd33..d600c95f88 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -10,6 +10,7 @@ use crate::state::revenue_share::{ }; use anchor_lang::prelude::*; +use crate::controller; use crate::controller::funding::settle_funding_payment; use crate::controller::position; use crate::controller::position::{ @@ -32,7 +33,9 @@ use crate::math::amm::calculate_amm_available_liquidity; use crate::math::amm_jit::calculate_amm_jit_liquidity; use crate::math::auction::{calculate_auction_params_for_trigger_order, calculate_auction_prices}; use crate::math::casting::Cast; -use crate::math::constants::{BASE_PRECISION_U64, PERP_DECIMALS, QUOTE_SPOT_MARKET_INDEX}; +use crate::math::constants::{ + BASE_PRECISION_U64, MARGIN_PRECISION, PERP_DECIMALS, QUOTE_SPOT_MARKET_INDEX, +}; use crate::math::fees::{determine_user_fee_tier, ExternalFillFees, FillFees}; use crate::math::fulfillment::{ determine_perp_fulfillment_methods, determine_spot_fulfillment_methods, @@ -81,7 +84,6 @@ use crate::validation; use crate::validation::order::{ validate_order, validate_order_for_force_reduce_only, validate_spot_order, }; -use crate::{controller, MARGIN_PRECISION}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs index ae6666328e..e26a4aa7ae 100644 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs @@ -33,12 +33,9 @@ pub mod amm_lp_jit { use crate::controller::position::PositionDirection; use crate::create_account_info; use crate::create_anchor_account_info; - use crate::math::constants::{ - PERCENTAGE_PRECISION_I128, PRICE_PRECISION_I64, QUOTE_PRECISION_I64, - }; + use crate::math::constants::{PRICE_PRECISION_I64, QUOTE_PRECISION_I64}; use crate::math::amm_jit::calculate_amm_jit_liquidity; - use crate::math::amm_spread::calculate_inventory_liquidity_ratio; use crate::math::constants::{ AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, PEG_PRECISION, PRICE_PRECISION, SPOT_BALANCE_PRECISION_U64, @@ -55,7 +52,6 @@ pub mod amm_lp_jit { use crate::state::user_map::{UserMap, UserStatsMap}; use crate::test_utils::*; use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; - use crate::validation::perp_market::validate_perp_market; use super::*; diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 960f1b052c..54223db8ea 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -6,8 +6,7 @@ use crate::controller::repeg::_update_amm; use crate::math::amm::calculate_market_open_bids_asks; use crate::math::constants::{ - AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, BASE_PRECISION, BASE_PRECISION_I64, - PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, + BASE_PRECISION, BASE_PRECISION_I64, PRICE_PRECISION_I64, PRICE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::math::oracle::OracleValidity; @@ -34,7 +33,6 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::SpotPosition; use crate::test_utils::get_anchor_account_bytes; use crate::test_utils::get_hardcoded_pyth_price; -use crate::QUOTE_PRECISION_I64; use anchor_lang::prelude::{AccountLoader, Clock}; use anchor_lang::Owner; use solana_program::pubkey::Pubkey; @@ -55,17 +53,7 @@ fn amm_pool_balance_liq_fees_example() { let perp_market_loader: AccountLoader = AccountLoader::try_from(&perp_market_account_info).unwrap(); - let perp_market_map = PerpMarketMap::load_one(&perp_market_account_info, true).unwrap(); - let now = 1725948560; - let clock_slot = 326319440; - let clock = Clock { - unix_timestamp: now, - slot: clock_slot, - ..Clock::default() - }; - - let mut state = State::default(); let mut prelaunch_oracle_price = PrelaunchOracle { price: PRICE_PRECISION_I64, @@ -79,9 +67,8 @@ fn amm_pool_balance_liq_fees_example() { prelaunch_oracle_price, &prelaunch_oracle_price_key, PrelaunchOracle, - oracle_account_info + _oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock_slot, None).unwrap(); let mut spot_market = SpotMarket { cumulative_deposit_interest: 11425141382, @@ -611,11 +598,11 @@ fn amm_ref_price_decay_tail_test() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -656,7 +643,7 @@ fn amm_ref_price_decay_tail_test() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -689,7 +676,7 @@ fn amm_ref_price_decay_tail_test() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -788,11 +775,11 @@ fn amm_ref_price_offset_decay_logic() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -833,7 +820,7 @@ fn amm_ref_price_offset_decay_logic() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -873,7 +860,7 @@ fn amm_ref_price_offset_decay_logic() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -962,11 +949,11 @@ fn amm_negative_ref_price_offset_decay_logic() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -1007,7 +994,7 @@ fn amm_negative_ref_price_offset_decay_logic() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1048,7 +1035,7 @@ fn amm_negative_ref_price_offset_decay_logic() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1148,11 +1135,11 @@ fn amm_perp_ref_offset() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -1194,7 +1181,7 @@ fn amm_perp_ref_offset() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, diff --git a/programs/drift/src/controller/repeg/tests.rs b/programs/drift/src/controller/repeg/tests.rs index 92d012dd8e..efbff0488d 100644 --- a/programs/drift/src/controller/repeg/tests.rs +++ b/programs/drift/src/controller/repeg/tests.rs @@ -258,7 +258,7 @@ pub fn update_amm_test_bad_oracle() { #[test] pub fn update_amm_larg_conf_test() { let now = 1662800000 + 60; - let mut slot = 81680085; + let slot = 81680085; let mut market = PerpMarket::default_btc_test(); assert_eq!(market.amm.base_asset_amount_with_amm, -1000000000); @@ -407,7 +407,7 @@ pub fn update_amm_larg_conf_test() { #[test] pub fn update_amm_larg_conf_w_neg_tfmd_test() { let now = 1662800000 + 60; - let mut slot = 81680085; + let slot = 81680085; let mut market = PerpMarket::default_btc_test(); market.amm.concentration_coef = 1414213; diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 83d6603fff..b1c19153c1 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -1385,7 +1385,7 @@ fn check_fee_collection_larger_nums() { #[test] fn test_multi_stage_borrow_rate_curve() { - let mut spot_market = SpotMarket { + let spot_market = SpotMarket { market_index: 0, oracle_source: OracleSource::QuoteAsset, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, @@ -1455,7 +1455,7 @@ fn test_multi_stage_borrow_rate_curve_sol() { let spot_market_loader: AccountLoader = AccountLoader::try_from(&sol_market_account_info).unwrap(); - let mut spot_market = spot_market_loader.load_mut().unwrap(); + let spot_market = spot_market_loader.load_mut().unwrap(); // Store all rates to verify monotonicity and smoothness later let mut last_rate = 0_u128; diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index 1a683556f0..a8ccf3a06e 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -1,6 +1,3 @@ -use anchor_lang::prelude::Pubkey; -use solana_program::pubkey; - pub mod pyth_program { use solana_program::declare_id; #[cfg(feature = "mainnet-beta")] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 58d27dfb0a..3cdf630592 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -9,20 +9,24 @@ use serum_dex::state::ToAlignedBytes; use std::convert::{identity, TryInto}; use std::mem::size_of; +use crate::controller; use crate::controller::token::{close_vault, initialize_immutable_owner, initialize_token_account}; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::{admin_hot_wallet, amm_spread_adjust_wallet, mm_oracle_crank_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load; use crate::math::casting::Cast; use crate::math::constants::{ AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, DEFAULT_LIQUIDATION_MARGIN_BUFFER_RATIO, - FEE_POOL_TO_REVENUE_POOL_THRESHOLD, GOV_SPOT_MARKET_INDEX, IF_FACTOR_PRECISION, - INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, - LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, - MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, - QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, - SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, + EPOCH_DURATION, FEE_ADJUSTMENT_MAX, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, GOV_SPOT_MARKET_INDEX, + IF_FACTOR_PRECISION, INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, + INSURANCE_SPECULATIVE_MAX, LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, + MAX_SQRT_K, MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, + QUOTE_PRECISION_I64, QUOTE_SPOT_MARKET_INDEX, SPOT_BALANCE_PRECISION, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, + TWENTY_FOUR_HOUR, }; use crate::math::cp_curve::get_update_k_result; use crate::math::helpers::get_proportion_u128; @@ -32,6 +36,7 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::math::{amm, bn}; +use crate::math_error; use crate::optional_accounts::get_token_mint; use crate::state::amm_cache::{AmmCache, CacheInfo, AMM_POSITIONS_CACHE}; use crate::state::events::{ @@ -77,12 +82,8 @@ use crate::validation::fee_structure::validate_fee_structure; use crate::validation::margin::{validate_margin, validate_margin_weights}; use crate::validation::perp_market::validate_perp_market; use crate::validation::spot_market::validate_borrow_rate; -use crate::{controller, QUOTE_PRECISION_I64}; -use crate::{get_then_update_id, EPOCH_DURATION}; -use crate::{load, FEE_ADJUSTMENT_MAX}; use crate::{load_mut, PTYH_PRICE_FEED_SEED_PREFIX}; use crate::{math, safe_decrement, safe_increment}; -use crate::{math_error, SPOT_BALANCE_PRECISION}; use anchor_spl::token_2022::spl_token_2022::extension::transfer_hook::TransferHook; use anchor_spl::token_2022::spl_token_2022::extension::{ diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 1d15b5ff6f..3e2381eb5f 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -6,6 +6,8 @@ use crate::error::ErrorCode; use crate::ids::{admin_hot_wallet, if_rebalance_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load_mut; +use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; use crate::optional_accounts::get_token_mint; use crate::state::insurance_fund_stake::{InsuranceFundStake, ProtocolIfSharesTransferConfig}; use crate::state::paused_operations::InsuranceFundOperation; @@ -23,7 +25,6 @@ use crate::{ spot_market_map::get_writable_spot_market_set_from_many, }, }; -use crate::{load_mut, QUOTE_SPOT_MARKET_INDEX}; use anchor_lang::solana_program::sysvar::instructions; use super::optional_accounts::get_token_interface; diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 81dc6aa269..e4e990d242 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -4,7 +4,6 @@ use std::convert::TryFrom; use anchor_lang::prelude::*; use anchor_lang::Discriminator; use anchor_spl::associated_token::get_associated_token_address_with_program_id; -use anchor_spl::token_interface::Mint; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use solana_program::instruction::Instruction; use solana_program::pubkey; @@ -31,8 +30,11 @@ use crate::ids::{ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load_mut; use crate::math::casting::Cast; -use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::constants::{ + GOV_SPOT_MARKET_INDEX, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, +}; use crate::math::lp_pool::perp_lp_pool_settlement; use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; @@ -87,14 +89,11 @@ use crate::state::user::{ use crate::state::user_map::{load_user_map, load_user_maps, UserMap, UserStatsMap}; use crate::state::zero_copy::AccountZeroCopyMut; use crate::state::zero_copy::ZeroCopyLoader; +use crate::validate; use crate::validation::sig_verification::verify_and_decode_ed25519_msg; use crate::validation::user::{validate_user_deletion, validate_user_is_idle}; -use crate::{ - controller, load, math, print_error, safe_decrement, OracleSource, GOV_SPOT_MARKET_INDEX, -}; -use crate::{load_mut, QUOTE_PRECISION_U64}; +use crate::{controller, load, math, print_error, safe_decrement, OracleSource}; use crate::{math_error, ID}; -use crate::{validate, QUOTE_PRECISION_I128}; use anchor_spl::associated_token::AssociatedToken; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index bc5910dae8..2e65dd0715 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -596,7 +596,6 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( amount_in: u64, ) -> Result<()> { // Check admin - let state = &ctx.accounts.state; let admin = &ctx.accounts.admin; #[cfg(feature = "anchor-test")] validate!( diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 5699598636..da92c5c75c 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -1536,7 +1536,6 @@ pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( } let oracle_data = oracle_map.get_price_data(&oracle_id)?; - let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; controller::spot_balance::update_spot_market_cumulative_interest( &mut spot_market, diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 3cb9d8e41a..ccc06c716f 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -21,6 +21,7 @@ use crate::controller::spot_position::{ update_spot_balances_and_cumulative_deposits_with_limits, }; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::admin_hot_wallet; use crate::ids::{ dflow_mainnet_aggregator_4, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, @@ -32,7 +33,9 @@ use crate::instructions::optional_accounts::{ get_referrer_and_referrer_stats, get_whitelist_token, load_maps, AccountMaps, }; 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::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; @@ -112,8 +115,6 @@ use crate::validation::position::validate_perp_position_with_perp_market; use crate::validation::user::validate_user_deletion; use crate::validation::whitelist::validate_whitelist_token; use crate::{controller, math}; -use crate::{get_then_update_id, QUOTE_SPOT_MARKET_INDEX}; -use crate::{load, THIRTEEN_DAY}; use crate::{load_mut, ExchangeStatus}; use anchor_lang::solana_program::sysvar::instructions; use anchor_spl::associated_token::AssociatedToken; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 2299adb213..dd9cb59e3a 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -6,7 +6,6 @@ use anchor_lang::prelude::*; use instructions::*; #[cfg(test)] -use math::amm; use math::{bn, constants::*}; use state::oracle::OracleSource; @@ -2154,8 +2153,8 @@ pub mod drift { pub fn end_lp_swap<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, - in_market_index: u16, - out_market_index: u16, + _in_market_index: u16, + _out_market_index: u16, ) -> Result<()> { handle_end_lp_swap(ctx) } diff --git a/programs/drift/src/math/amm.rs b/programs/drift/src/math/amm.rs index 0407f627cf..914dffe621 100644 --- a/programs/drift/src/math/amm.rs +++ b/programs/drift/src/math/amm.rs @@ -10,8 +10,8 @@ use crate::math::casting::Cast; use crate::math::constants::{ BID_ASK_SPREAD_PRECISION_I128, CONCENTRATION_PRECISION, DEFAULT_MAX_TWAP_UPDATE_PRICE_BAND_DENOMINATOR, FIVE_MINUTE, ONE_HOUR, ONE_MINUTE, - PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, - PRICE_TO_PEG_PRECISION_RATIO, + PERCENTAGE_PRECISION_U64, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, PRICE_TO_PEG_PRECISION_RATIO, }; use crate::math::orders::standardize_base_asset_amount; use crate::math::quote_asset::reserve_to_asset_amount; @@ -19,7 +19,7 @@ use crate::math::stats::{calculate_new_twap, calculate_rolling_sum, calculate_we use crate::state::oracle::{MMOraclePriceData, OraclePriceData}; use crate::state::perp_market::AMM; use crate::state::state::PriceDivergenceGuardRails; -use crate::{validate, PERCENTAGE_PRECISION_U64}; +use crate::validate; use super::helpers::get_proportion_u128; use crate::math::safe_math::SafeMath; diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index 88049e246f..6378ea9cc8 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -1,7 +1,7 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; -use crate::math::constants::AUCTION_DERIVE_PRICE_FRACTION; +use crate::math::constants::{AUCTION_DERIVE_PRICE_FRACTION, MAX_PREDICTION_MARKET_PRICE}; use crate::math::orders::standardize_price; use crate::math::safe_math::SafeMath; use crate::msg; @@ -11,7 +11,7 @@ use crate::state::user::{Order, OrderBitFlag, OrderType}; use crate::state::fill_mode::FillMode; use crate::state::perp_market::{AMMAvailability, PerpMarket}; -use crate::{OrderParams, MAX_PREDICTION_MARKET_PRICE}; +use crate::OrderParams; use std::cmp::min; use super::orders::get_posted_slot_from_clock_slot; diff --git a/programs/drift/src/math/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index ca7de7e987..b6c8b667b7 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -1,14 +1,10 @@ use crate::controller::amm::update_spreads; use crate::controller::position::PositionDirection; -use crate::math::amm::calculate_bid_ask_bounds; -use crate::math::constants::BASE_PRECISION; -use crate::math::constants::CONCENTRATION_PRECISION; use crate::math::constants::{ - BASE_PRECISION_U64, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, QUOTE_PRECISION_I64, + MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, QUOTE_PRECISION_I64, }; use crate::math::cp_curve::*; use crate::state::perp_market::AMM; -use crate::state::user::PerpPosition; #[test] fn k_update_results_bound_flag() { @@ -331,10 +327,6 @@ fn amm_spread_adj_logic() { // let (t_price, _t_qar, _t_bar) = calculate_terminal_price_and_reserves(&market.amm).unwrap(); // market.amm.terminal_quote_asset_reserve = _t_qar; - let mut position = PerpPosition { - ..PerpPosition::default() - }; - // todo fix this market.amm.base_asset_amount_per_lp = 1; diff --git a/programs/drift/src/math/fees.rs b/programs/drift/src/math/fees.rs index 4b358b071a..8431be24bf 100644 --- a/programs/drift/src/math/fees.rs +++ b/programs/drift/src/math/fees.rs @@ -16,8 +16,8 @@ use crate::math::safe_math::SafeMath; use crate::state::state::{FeeStructure, FeeTier, OrderFillerRewardStructure}; use crate::state::user::{MarketType, UserStats}; +use crate::math::constants::{FEE_ADJUSTMENT_MAX, QUOTE_PRECISION_U64}; use crate::msg; -use crate::{FEE_ADJUSTMENT_MAX, QUOTE_PRECISION_U64}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/math/fuel.rs b/programs/drift/src/math/fuel.rs index 2cd139d9f3..5069761c98 100644 --- a/programs/drift/src/math/fuel.rs +++ b/programs/drift/src/math/fuel.rs @@ -1,9 +1,9 @@ use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{FUEL_WINDOW_U128, QUOTE_PRECISION, QUOTE_PRECISION_U64}; use crate::math::safe_math::SafeMath; use crate::state::perp_market::PerpMarket; use crate::state::spot_market::SpotMarket; -use crate::{FUEL_WINDOW_U128, QUOTE_PRECISION, QUOTE_PRECISION_U64}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/math/insurance.rs b/programs/drift/src/math/insurance.rs index 1232a19451..4658f97db6 100644 --- a/programs/drift/src/math/insurance.rs +++ b/programs/drift/src/math/insurance.rs @@ -1,7 +1,8 @@ -use crate::{msg, PRICE_PRECISION}; +use crate::msg; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::PRICE_PRECISION; use crate::math::helpers::{get_proportion_u128, log10_iter}; use crate::math::safe_math::SafeMath; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 24a54afc59..dbe608ceaa 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -12,6 +12,7 @@ use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_l use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; +use crate::math::constants::{BASE_PRECISION, LIQUIDATION_FEE_INCREASE_PER_SLOT}; use crate::math::spot_swap::calculate_swap_price; use crate::msg; use crate::state::margin_calculation::MarginContext; @@ -21,10 +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::user::{OrderType, User}; -use crate::{ - validate, MarketType, OrderParams, PositionDirection, BASE_PRECISION, - LIQUIDATION_FEE_INCREASE_PER_SLOT, -}; +use crate::{validate, MarketType, OrderParams, PositionDirection}; pub const LIQUIDATION_FEE_ADJUST_GRACE_PERIOD_SLOTS: u64 = 1_500; // ~10 minutes diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs index 448721acb4..2e4bf00532 100644 --- a/programs/drift/src/math/lp_pool.rs +++ b/programs/drift/src/math/lp_pool.rs @@ -4,6 +4,7 @@ pub mod perp_lp_pool_settlement { use crate::error::ErrorCode; use crate::math::casting::Cast; + use crate::math::constants::QUOTE_PRECISION_U64; use crate::state::spot_market::SpotBalanceType; use crate::{ math::safe_math::SafeMath, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 9d227bfe8b..1ae78542d5 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -6,9 +6,9 @@ use crate::math::constants::{ }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; -use crate::MARGIN_PRECISION; -use crate::{validate, PRICE_PRECISION_I128}; -use crate::{validation, PRICE_PRECISION_I64}; +use crate::math::constants::{MARGIN_PRECISION, PRICE_PRECISION_I128, PRICE_PRECISION_I64}; +use crate::validate; +use crate::validation; use crate::math::casting::Cast; use crate::math::funding::calculate_funding_payment; diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 2e9e25da44..d4b1eefd2e 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -1,9 +1,5 @@ #[cfg(test)] mod test { - use num_integer::Roots; - - use crate::amm::calculate_swap_output; - use crate::controller::amm::SwapDirection; use crate::math::constants::{ AMM_RESERVE_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, QUOTE_PRECISION, QUOTE_PRECISION_I64, SPOT_IMF_PRECISION, @@ -18,6 +14,7 @@ mod test { PRICE_PRECISION_I64, QUOTE_PRECISION_U64, SPOT_BALANCE_PRECISION, SPOT_CUMULATIVE_INTEREST_PRECISION, }; + use num_integer::Roots; #[test] fn asset_tier_checks() { diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index b4495afd5c..7f4844da5a 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -8,17 +8,16 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::calculate_amm_available_liquidity; use crate::math::casting::Cast; -use crate::state::protected_maker_mode_config::ProtectedMakerParams; -use crate::state::user::OrderBitFlag; -use crate::PERCENTAGE_PRECISION_I128; -use crate::{ - load, math, FeeTier, BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, MARGIN_PRECISION_I128, +use crate::math::constants::{ + BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, MAX_PREDICTION_MARKET_PRICE, MAX_PREDICTION_MARKET_PRICE_I64, OPEN_ORDER_MARGIN_REQUIREMENT, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I128, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, - SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, + PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I128, PRICE_PRECISION_U64, + QUOTE_PRECISION_I128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, }; +use crate::state::protected_maker_mode_config::ProtectedMakerParams; +use crate::state::user::OrderBitFlag; +use crate::{load, math, FeeTier}; -use crate::math::constants::MARGIN_PRECISION_U128; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 52b0e012a8..25e625d139 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -3348,7 +3348,7 @@ mod calculate_max_perp_order_size { &lazer_program, ); - let mut account_infos = vec![ + let account_infos = vec![ usdc_oracle_info, sol_oracle_info, eth_oracle_info, diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index 8201123f09..e40c75205d 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -5,15 +5,14 @@ use crate::math::amm; use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_RESERVE_PRECISION_I128, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, - PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, + AMM_RESERVE_PRECISION_I128, BASE_PRECISION, MAX_PREDICTION_MARKET_PRICE_U128, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, }; use crate::math::pnl::calculate_pnl; use crate::math::safe_math::SafeMath; use crate::state::perp_market::{ContractType, AMM}; use crate::state::user::PerpPosition; -use crate::{BASE_PRECISION, MAX_PREDICTION_MARKET_PRICE_U128}; pub fn calculate_base_asset_value_and_pnl( base_asset_amount: i128, diff --git a/programs/drift/src/math/spot_swap.rs b/programs/drift/src/math/spot_swap.rs index 10bcc87e16..8087786c05 100644 --- a/programs/drift/src/math/spot_swap.rs +++ b/programs/drift/src/math/spot_swap.rs @@ -1,12 +1,13 @@ use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{PRICE_PRECISION, SPOT_WEIGHT_PRECISION_U128}; use crate::math::margin::MarginRequirementType; use crate::math::orders::{calculate_fill_price, validate_fill_price_within_price_bands}; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{get_strict_token_value, get_token_value}; use crate::state::oracle::StrictOraclePrice; use crate::state::spot_market::SpotMarket; -use crate::{PositionDirection, PRICE_PRECISION, SPOT_WEIGHT_PRECISION_U128}; +use crate::PositionDirection; #[cfg(test)] mod tests; diff --git a/programs/drift/src/state/insurance_fund_stake.rs b/programs/drift/src/state/insurance_fund_stake.rs index bd92019969..25d81f4b0d 100644 --- a/programs/drift/src/state/insurance_fund_stake.rs +++ b/programs/drift/src/state/insurance_fund_stake.rs @@ -1,12 +1,13 @@ use crate::error::DriftResult; use crate::error::ErrorCode; +use crate::math::constants::EPOCH_DURATION; use crate::math::safe_math::SafeMath; +use crate::math_error; use crate::safe_decrement; use crate::safe_increment; use crate::state::spot_market::SpotMarket; use crate::state::traits::Size; use crate::validate; -use crate::{math_error, EPOCH_DURATION}; use anchor_lang::prelude::*; #[cfg(test)] diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 033d67c780..93d3075159 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::f32::consts::E; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index 19c15b45be..24cab5e03d 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -1324,14 +1324,7 @@ mod swap_tests { let mut prev_in_fee_bps = 0_i128; let mut prev_out_fee_bps = 0_i128; for in_amount in in_amounts.iter() { - let ( - in_amount_result, - out_amount, - in_fee, - out_fee, - in_token_amount_pre, - out_token_amount_pre, - ) = get_swap_amounts( + let (in_amount_result, out_amount, in_fee, out_fee, _, _) = get_swap_amounts( 0, 0, 0, diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 4a0c299e4e..cf2a39ac29 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -1,5 +1,8 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::{ + AMM_RESERVE_PRECISION_I128, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, +}; use crate::math::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; use crate::math::margin::MarginRequirementType; use crate::math::safe_math::SafeMath; @@ -8,9 +11,7 @@ use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::PerpMarket; use crate::state::spot_market::SpotMarket; use crate::state::user::{PerpPosition, User}; -use crate::{ - validate, MarketType, AMM_RESERVE_PRECISION_I128, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, -}; +use crate::{validate, MarketType}; use anchor_lang::{prelude::*, solana_program::msg}; #[derive(Clone, Copy, Debug)] diff --git a/programs/drift/src/state/oracle.rs b/programs/drift/src/state/oracle.rs index 2622e5d928..0275a5549a 100644 --- a/programs/drift/src/state/oracle.rs +++ b/programs/drift/src/state/oracle.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; -use bytemuck::{Pod, Zeroable}; use std::cell::Ref; use std::convert::TryFrom; diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 95ccf8424d..422857fed2 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -1,15 +1,15 @@ use crate::controller::position::PositionDirection; use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{ + MAX_PREDICTION_MARKET_PRICE_I64, ONE_HUNDRED_THOUSAND_QUOTE, PERCENTAGE_PRECISION_I64, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, +}; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::state::events::OrderActionExplanation; use crate::state::perp_market::{ContractTier, PerpMarket}; use crate::state::user::{MarketType, OrderTriggerCondition, OrderType}; -use crate::{ - MAX_PREDICTION_MARKET_PRICE_I64, ONE_HUNDRED_THOUSAND_QUOTE, PERCENTAGE_PRECISION_I64, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, -}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use std::ops::Div; diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 3aa2b02d9d..95b23019b3 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -11,9 +11,7 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::{self}; use crate::math::casting::Cast; #[cfg(test)] -use crate::math::constants::{ - AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64, -}; +use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; use crate::math::constants::{ AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 84a0cdd89d..01bbe6f4a7 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -9,7 +9,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, SPOT_WEIGHT_PRECISION_U128, + AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, PERCENTAGE_PRECISION, + SPOT_WEIGHT_PRECISION_U128, }; #[cfg(test)] use crate::math::constants::{PRICE_PRECISION_I64, SPOT_CUMULATIVE_INTEREST_PRECISION}; @@ -25,7 +26,7 @@ use crate::state::oracle::{HistoricalIndexData, HistoricalOracleData, OracleSour use crate::state::paused_operations::{InsuranceFundOperation, SpotOperation}; use crate::state::perp_market::{MarketStatus, PoolBalance}; use crate::state::traits::{MarketIndexOffset, Size}; -use crate::{validate, PERCENTAGE_PRECISION}; +use crate::validate; use super::oracle_map::OracleIdentifier; diff --git a/programs/drift/src/state/state.rs b/programs/drift/src/state/state.rs index 10486403b8..07e9213396 100644 --- a/programs/drift/src/state/state.rs +++ b/programs/drift/src/state/state.rs @@ -3,12 +3,12 @@ use enumflags2::BitFlags; use crate::error::DriftResult; use crate::math::constants::{ - FEE_DENOMINATOR, FEE_PERCENTAGE_DENOMINATOR, MAX_REFERRER_REWARD_EPOCH_UPPER_BOUND, + FEE_DENOMINATOR, FEE_PERCENTAGE_DENOMINATOR, LAMPORTS_PER_SOL_U64, + MAX_REFERRER_REWARD_EPOCH_UPPER_BOUND, PERCENTAGE_PRECISION_U64, }; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::state::traits::Size; -use crate::{LAMPORTS_PER_SOL_U64, PERCENTAGE_PRECISION_U64}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b36607e3dc..03a823fb41 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -3,8 +3,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::auction::{calculate_auction_price, is_auction_complete}; use crate::math::casting::Cast; use crate::math::constants::{ - EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, OPEN_ORDER_MARGIN_REQUIREMENT, - QUOTE_SPOT_MARKET_INDEX, THIRTY_DAY, + EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, MAX_PREDICTION_MARKET_PRICE, + OPEN_ORDER_MARGIN_REQUIREMENT, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, + SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, THIRTY_DAY, }; use crate::math::margin::MarginRequirementType; use crate::math::orders::{ @@ -18,15 +19,15 @@ use crate::math::spot_balance::{ get_signed_token_amount, get_strict_token_value, get_token_amount, get_token_value, }; use crate::math::stats::calculate_rolling_sum; +use crate::math_error; use crate::msg; +use crate::safe_increment; use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::ContractType; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::traits::Size; -use crate::{get_then_update_id, ID, QUOTE_PRECISION_U64}; -use crate::{math_error, SPOT_WEIGHT_PRECISION_I128}; -use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; -use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; +use crate::validate; +use crate::{get_then_update_id, ID}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use bytemuck::{Pod, Zeroable}; diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 5bd2b154f5..4b2392d1ba 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2319,9 +2319,7 @@ mod update_referrer_status { } mod update_open_bids_and_asks { - use crate::state::user::{ - Order, OrderBitFlag, OrderTriggerCondition, OrderType, PositionDirection, - }; + use crate::state::user::{Order, OrderBitFlag, OrderTriggerCondition, OrderType}; #[test] fn test_regular_limit_order() { diff --git a/programs/drift/src/validation/margin.rs b/programs/drift/src/validation/margin.rs index f790d31121..49c993c89f 100644 --- a/programs/drift/src/validation/margin.rs +++ b/programs/drift/src/validation/margin.rs @@ -1,10 +1,10 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::constants::{ - LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MAX_MARGIN_RATIO, MIN_MARGIN_RATIO, - SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, + HIGH_LEVERAGE_MIN_MARGIN_RATIO, LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MAX_MARGIN_RATIO, + MIN_MARGIN_RATIO, SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::msg; -use crate::{validate, HIGH_LEVERAGE_MIN_MARGIN_RATIO}; +use crate::validate; pub fn validate_margin( margin_ratio_initial: u32, diff --git a/programs/drift/src/validation/order.rs b/programs/drift/src/validation/order.rs index 92dd4da465..74452a020f 100644 --- a/programs/drift/src/validation/order.rs +++ b/programs/drift/src/validation/order.rs @@ -4,13 +4,14 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::MAX_PREDICTION_MARKET_PRICE; use crate::math::orders::{ calculate_base_asset_amount_to_fill_up_to_limit_price, is_multiple_of_step_size, }; use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::PerpMarket; use crate::state::user::{Order, OrderTriggerCondition, OrderType}; -use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; +use crate::validate; #[cfg(test)] mod test; diff --git a/programs/drift/src/validation/perp_market.rs b/programs/drift/src/validation/perp_market.rs index e55257659f..30bce4dedd 100644 --- a/programs/drift/src/validation/perp_market.rs +++ b/programs/drift/src/validation/perp_market.rs @@ -1,12 +1,12 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; -use crate::math::constants::MAX_BASE_ASSET_AMOUNT_WITH_AMM; +use crate::math::constants::{BID_ASK_SPREAD_PRECISION, MAX_BASE_ASSET_AMOUNT_WITH_AMM}; use crate::math::safe_math::SafeMath; use crate::msg; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; -use crate::{validate, BID_ASK_SPREAD_PRECISION}; +use crate::validate; #[allow(clippy::comparison_chain)] pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { @@ -81,10 +81,10 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.amm.quote_asset_reserve )?; - let invariant_sqrt_u192 = crate::bn::U192::from(market.amm.sqrt_k); + let invariant_sqrt_u192 = crate::math::bn::U192::from(market.amm.sqrt_k); let invariant = invariant_sqrt_u192.safe_mul(invariant_sqrt_u192)?; let quote_asset_reserve = invariant - .safe_div(crate::bn::U192::from(market.amm.base_asset_reserve))? + .safe_div(crate::math::bn::U192::from(market.amm.base_asset_reserve))? .try_to_u128()?; let rounding_diff = quote_asset_reserve diff --git a/programs/drift/src/validation/user.rs b/programs/drift/src/validation/user.rs index 3f527fed0f..999b342f45 100644 --- a/programs/drift/src/validation/user.rs +++ b/programs/drift/src/validation/user.rs @@ -1,8 +1,9 @@ use crate::error::{DriftResult, ErrorCode}; +use crate::math::constants::THIRTEEN_DAY; use crate::msg; use crate::state::spot_market::SpotBalanceType; use crate::state::user::{User, UserStats}; -use crate::{validate, State, THIRTEEN_DAY}; +use crate::{validate, State}; pub fn validate_user_deletion( user: &User, diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index b1a867a851..57da474bb7 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -55,45 +55,45 @@ 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; - } + 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; + } | { - 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; - oracleAccountSubscriber?: new ( - accountName: string, - program: Program, - accountPublicKey: PublicKey, - decodeBuffer?: (buffer: Buffer) => any, - resubOpts?: ResubOpts, - commitment?: Commitment - ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - } + 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; + oracleAccountSubscriber?: new ( + accountName: string, + program: Program, + accountPublicKey: PublicKey, + decodeBuffer?: (buffer: Buffer) => any, + resubOpts?: ResubOpts, + commitment?: Commitment + ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; + } | { - type: 'polling'; - accountLoader: BulkAccountLoader; - }; + type: 'polling'; + accountLoader: BulkAccountLoader; + }; From 28ed029e2d4be03f846bff965519f3cd9dce770b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 22 Oct 2025 15:10:33 -0400 Subject: [PATCH 140/159] make build --- programs/drift/src/instructions/keeper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 297351ba42..6c38ec6c47 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -771,7 +771,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( if let Some(isolated_position_deposit) = verified_message_and_signature.isolated_position_deposit { transfer_isolated_perp_position_deposit( taker, - taker_stats, + Some(taker_stats), perp_market_map, spot_market_map, oracle_map, From b0a2b674e25a5b7e94dbf2fb85891687c973056f Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 23 Oct 2025 14:22:13 -0400 Subject: [PATCH 141/159] isolated pos deposit sdk --- sdk/src/driftClient.ts | 39 +++++++++++++++++++++++++-------------- sdk/src/types.ts | 2 ++ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index abff8433df..914c580f4b 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -6827,12 +6827,6 @@ export class DriftClient { precedingIxs: TransactionInstruction[] = [], overrideCustomIxIndex?: number ): Promise { - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [takerInfo.takerUserAccount], - useMarketLastSlotCache: false, - readablePerpMarketIndex: marketIndex, - }); - const isDelegateSigner = takerInfo.signingAuthority.equals( takerInfo.takerUserAccount.delegate ); @@ -6841,22 +6835,39 @@ export class DriftClient { signedSignedMsgOrderParams.orderParams.toString(), 'hex' ); + + let isIsolatedPositionDeposit = false; + let isHLMode = false; try { - const { signedMsgOrderParams } = this.decodeSignedMsgOrderParamsMessage( + const message = this.decodeSignedMsgOrderParamsMessage( borshBuf, isDelegateSigner ); - if (isUpdateHighLeverageMode(signedMsgOrderParams.bitFlags)) { - remainingAccounts.push({ - pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), - isWritable: true, - isSigner: false, - }); - } + isIsolatedPositionDeposit = message.isolatedPositionDeposit?.gt( + new BN(0) + ); + isHLMode = isUpdateHighLeverageMode(message.signedMsgOrderParams.bitFlags); } catch (err) { console.error('invalid signed order encoding'); } + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [takerInfo.takerUserAccount], + useMarketLastSlotCache: false, + readablePerpMarketIndex: marketIndex, + writableSpotMarketIndexes: isIsolatedPositionDeposit + ? [QUOTE_SPOT_MARKET_INDEX] + : undefined, + }); + + if (isHLMode) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + const messageLengthBuffer = Buffer.alloc(2); messageLengthBuffer.writeUInt16LE( signedSignedMsgOrderParams.orderParams.length diff --git a/sdk/src/types.ts b/sdk/src/types.ts index f263e2b9cc..d2911435b9 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1312,6 +1312,7 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDeposit?: BN | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1322,6 +1323,7 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDeposit?: BN | null; }; export type SignedMsgTriggerOrderParams = { From 3bd6eeb51dc71272a6428405efb74872ac215f1e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 23 Oct 2025 14:33:49 -0400 Subject: [PATCH 142/159] test working --- programs/drift/src/instructions/keeper.rs | 7 ++++--- programs/drift/src/state/spot_market_map.rs | 15 +++++++++++++++ sdk/src/idl/drift.json | 15 ++++++++++++--- test-scripts/single-anchor-test.sh | 2 +- tests/placeAndMakeSignedMsgBankrun.ts | 4 +++- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 6c38ec6c47..c77386215f 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -617,7 +617,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, @@ -642,7 +642,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( 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, state, @@ -659,7 +659,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( 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>, state: &State, @@ -769,6 +769,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( } 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), 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/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 8455798584..d2ac41ed64 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -10281,6 +10281,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -10334,6 +10340,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -12534,9 +12546,6 @@ }, { "name": "UpdateHighLeverageMode" - }, - { - "name": "EnableAutoTransfer" } ] } diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index a9f48da728..e27480eeba 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -7,7 +7,7 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json test_files=( - spotDepositWithdraw22ScaledUI.ts + placeAndMakeSignedMsgBankrun.ts ) for test_file in ${test_files[@]}; do diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 3b23722445..8c2921dc37 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -1605,7 +1605,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() ); @@ -1657,6 +1657,7 @@ describe('place and make signedMsg order', () => { stopLossOrderParams: null, takeProfitOrderParams: null, maxMarginRatio: 100, + isolatedPositionDeposit: usdcAmount, }; const signedOrderParams = takerDriftClient.signSignedMsgOrderParamsMessage( @@ -1699,6 +1700,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(); From 3bd7525e57e095c9441d687af7890d22ff8fc38d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 23 Oct 2025 16:03:05 -0400 Subject: [PATCH 143/159] move the auto transfer --- programs/drift/src/controller/pnl.rs | 16 ----------- programs/drift/src/instructions/keeper.rs | 35 +++++++++++++++++++++++ programs/drift/src/state/user.rs | 1 + 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index d1640c1251..72bfc98bf4 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -58,7 +58,6 @@ pub fn settle_pnl( mut mode: SettlePnlMode, ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; - let slot = clock.slot; let now = clock.unix_timestamp; let tvl_before; let deposits_balance_before; @@ -344,21 +343,6 @@ pub fn settle_pnl( drop(perp_market); drop(spot_market); - if user.perp_positions[position_index].can_transfer_isolated_position_deposit() { - transfer_isolated_perp_position_deposit( - user, - None, - perp_market_map, - spot_market_map, - oracle_map, - slot, - now, - 0, - market_index, - i64::MIN, - )?; - } - let perp_market = perp_market_map.get_ref(&market_index)?; let spot_market = spot_market_map.get_quote_spot_market()?; diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index c77386215f..fe924e6971 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -17,6 +17,7 @@ use crate::controller::liquidation::{ liquidate_spot_with_swap_begin, liquidate_spot_with_swap_end, }; use crate::controller::orders::cancel_orders; +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}; @@ -980,6 +981,23 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + 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)?; @@ -1064,6 +1082,23 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + + 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()?; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 4a117c1a97..176fba0bd6 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1284,6 +1284,7 @@ impl PerpPosition { 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() From b78695b3a9c689a5d9bd0ef78ddfb614730bd87d Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:51:56 -0700 Subject: [PATCH 144/159] sync new oracle map --- programs/drift/src/instructions/lp_pool.rs | 8 ++++++++ programs/drift/src/state/lp_pool.rs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index a0e311d236..e8f33939b9 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -336,6 +336,7 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -349,6 +350,7 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { @@ -559,6 +561,7 @@ pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -572,6 +575,7 @@ pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); @@ -699,6 +703,7 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -918,6 +923,7 @@ pub fn handle_view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -1065,6 +1071,7 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let out_oracle = *out_oracle; @@ -1320,6 +1327,7 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let out_oracle = out_oracle.clone(); diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 62c22c8ac3..52ae06744b 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -738,6 +738,7 @@ impl LPPool { spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action( oracle_and_validity.1, @@ -1705,6 +1706,7 @@ pub fn update_constituent_target_base_for_derivatives( parent_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action( parent_oracle_price_and_validity.1, @@ -1742,6 +1744,7 @@ pub fn update_constituent_target_base_for_derivatives( constituent_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action( constituent_oracle_price_and_validity.1, From 00139c4ea1073417fdb71388ce2964cdee00eb8c Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:43:10 -0700 Subject: [PATCH 145/159] Use lp pool id (#1992) * add lp pool id and replace lp pool name * add settle perp market enforcement with lp pool id * constituent map fix --- programs/drift/src/error.rs | 2 + programs/drift/src/instructions/admin.rs | 23 +- programs/drift/src/instructions/keeper.rs | 10 + programs/drift/src/instructions/lp_admin.rs | 23 +- programs/drift/src/instructions/lp_pool.rs | 8 +- programs/drift/src/lib.rs | 13 +- programs/drift/src/state/lp_pool.rs | 16 +- programs/drift/src/state/perp_market.rs | 6 +- sdk/src/addresses/pda.ts | 4 +- sdk/src/adminClient.ts | 116 +++++----- sdk/src/constituentMap/constituentMap.ts | 13 +- sdk/src/driftClient.ts | 32 +-- sdk/src/idl/drift.json | 108 ++++----- sdk/src/types.ts | 3 +- tests/lpPool.ts | 232 ++++++++++---------- tests/lpPoolCUs.ts | 17 +- tests/lpPoolSwap.ts | 23 +- 17 files changed, 353 insertions(+), 296 deletions(-) diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index f95399b190..ab38f477bc 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -690,6 +690,8 @@ pub enum ErrorCode { InvalidConstituentOperation, #[msg("Unauthorized for operation")] Unauthorized, + #[msg("Invalid Lp Pool Id for Operation")] + InvalidLpPoolId, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 6234101d05..b260899694 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -714,6 +714,7 @@ pub fn handle_initialize_perp_market( curve_update_intensity: u8, amm_jit_intensity: u8, name: [u8; 32], + lp_pool_id: u8, ) -> Result<()> { msg!("perp market {}", market_index); let perp_market_pubkey = ctx.accounts.perp_market.to_account_info().key; @@ -1001,7 +1002,8 @@ pub fn handle_initialize_perp_market( lp_exchange_fee_excluscion_scalar: 0, lp_paused_operations: 0, last_fill_price: 0, - padding: [0; 24], + lp_pool_id, + padding: [0; 23], amm: AMM { oracle: *ctx.accounts.oracle.key, oracle_source, @@ -2829,6 +2831,25 @@ pub fn handle_update_perp_liquidation_fee( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_lp_pool_id( + ctx: Context, + lp_pool_id: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + + msg!( + "updating perp market {} lp pool id: {} -> {}", + perp_market.market_index, + perp_market.lp_pool_id, + lp_pool_id + ); + perp_market.lp_pool_id = lp_pool_id; + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index e4e990d242..e3fda5d45f 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -3356,6 +3356,16 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( for (_, perp_market_loader) in perp_market_map.0.iter() { let mut perp_market = perp_market_loader.load_mut()?; + if lp_pool.lp_pool_id != perp_market.lp_pool_id { + msg!( + "Perp market {} does not have the same lp pool id as the lp pool being settled to: {} != {}", + perp_market.market_index, + perp_market.lp_pool_id, + lp_pool.lp_pool_id + ); + return Err(ErrorCode::InvalidLpPoolId.into()); + } + if perp_market.lp_status == 0 || PerpLpOperation::is_operation_paused( perp_market.lp_paused_operations, diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 2e65dd0715..bd475b1282 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -36,7 +36,7 @@ use super::optional_accounts::get_token_interface; pub fn handle_initialize_lp_pool( ctx: Context, - name: [u8; 32], + lp_pool_id: u8, min_mint_fee: i64, max_aum: u128, max_settle_quote_amount_per_market: u64, @@ -59,7 +59,6 @@ pub fn handle_initialize_lp_pool( )?; *lp_pool = LPPool { - name, pubkey: ctx.accounts.lp_pool.key(), mint: mint.key(), constituent_target_base: ctx.accounts.constituent_target_base.key(), @@ -84,7 +83,8 @@ pub fn handle_initialize_lp_pool( xi: 2, target_oracle_delay_fee_bps_per_10_slots: 0, target_position_delay_fee_bps_per_10_slots: 0, - padding: [0u8; 15], + lp_pool_id, + padding: [0u8; 14], whitelist_mint, }; @@ -598,11 +598,14 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( // Check admin let admin = &ctx.accounts.admin; #[cfg(feature = "anchor-test")] - validate!( - admin.key() == admin_hot_wallet::id() || admin.key() == state.admin, - ErrorCode::Unauthorized, - "Wrong signer for lp taker swap" - )?; + { + let state = &ctx.accounts.state; + validate!( + admin.key() == admin_hot_wallet::id() || admin.key() == state.admin, + ErrorCode::Unauthorized, + "Wrong signer for lp taker swap" + )?; + } #[cfg(not(feature = "anchor-test"))] validate!( admin.key() == lp_pool_swap_wallet::id(), @@ -1008,14 +1011,14 @@ pub fn handle_override_amm_cache_info<'c: 'info, 'info>( #[derive(Accounts)] #[instruction( - name: [u8; 32], + id: u8, )] pub struct InitializeLpPool<'info> { #[account(mut)] pub admin: Signer<'info>, #[account( init, - seeds = [b"lp_pool", name.as_ref()], + seeds = [b"lp_pool", id.to_le_bytes().as_ref()], space = LPPool::SIZE, bump, payer = admin diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index a0e311d236..9f75a6d2f1 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -770,10 +770,10 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( in_constituent.record_swap_fees(in_fee_amount)?; lp_pool.record_mint_redeem_fees(lp_fee_amount)?; - let lp_name = lp_pool.name; + let lp_pool_id = lp_pool.lp_pool_id; let lp_bump = lp_pool.bump; - let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_name, &lp_bump); + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_pool_id, &lp_bump); drop(lp_pool); @@ -1166,10 +1166,10 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( out_constituent.record_swap_fees(out_fee_amount)?; lp_pool.record_mint_redeem_fees(lp_fee_amount)?; - let lp_name = lp_pool.name; + let lp_pool_id = lp_pool.lp_pool_id; let lp_bump = lp_pool.bump; - let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_name, &lp_bump); + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_pool_id, &lp_bump); drop(lp_pool); diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 2c7ee3320c..bc21e2d38d 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -981,6 +981,7 @@ pub mod drift { curve_update_intensity: u8, amm_jit_intensity: u8, name: [u8; 32], + lp_pool_id: u8, ) -> Result<()> { handle_initialize_perp_market( ctx, @@ -1009,6 +1010,7 @@ pub mod drift { curve_update_intensity, amm_jit_intensity, name, + lp_pool_id, ) } @@ -1200,6 +1202,13 @@ pub mod drift { handle_update_perp_liquidation_fee(ctx, liquidator_fee, if_liquidation_fee) } + pub fn update_perp_market_lp_pool_id( + ctx: Context, + lp_pool_id: u8, + ) -> Result<()> { + handle_update_perp_lp_pool_id(ctx, lp_pool_id) + } + pub fn update_insurance_fund_unstaking_period( ctx: Context, insurance_fund_unstaking_period: i64, @@ -1795,7 +1804,7 @@ pub mod drift { pub fn initialize_lp_pool( ctx: Context, - name: [u8; 32], + lp_pool_id: u8, min_mint_fee: i64, max_aum: u128, max_settle_quote_amount_per_market: u64, @@ -1803,7 +1812,7 @@ pub mod drift { ) -> Result<()> { handle_initialize_lp_pool( ctx, - name, + lp_pool_id, min_mint_fee, max_aum, max_settle_quote_amount_per_market, diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 58255d420d..4acab91a58 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -63,8 +63,6 @@ mod tests; #[derive(Default, Debug)] #[repr(C)] pub struct LPPool { - /// name of vault, TODO: check type + size - pub name: [u8; 32], /// address of the vault. pub pubkey: Pubkey, // vault token mint @@ -127,11 +125,13 @@ pub struct LPPool { pub target_oracle_delay_fee_bps_per_10_slots: u8, pub target_position_delay_fee_bps_per_10_slots: u8, - pub padding: [u8; 15], + pub lp_pool_id: u8, + + pub padding: [u8; 14], } impl Size for LPPool { - const SIZE: usize = 376; + const SIZE: usize = 344; } impl LPPool { @@ -803,8 +803,12 @@ impl LPPool { Ok((aum_u128, crypto_delta, derivative_groups)) } - pub fn get_lp_pool_signer_seeds<'a>(name: &'a [u8; 32], bump: &'a u8) -> [&'a [u8]; 3] { - [LP_POOL_PDA_SEED.as_ref(), name, bytemuck::bytes_of(bump)] + pub fn get_lp_pool_signer_seeds<'a>(lp_pool_id: &'a u8, bump: &'a u8) -> [&'a [u8]; 3] { + [ + LP_POOL_PDA_SEED.as_ref(), + bytemuck::bytes_of(lp_pool_id), + bytemuck::bytes_of(bump), + ] } } diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 90d6c8027b..5dfe3d9147 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -251,7 +251,8 @@ pub struct PerpMarket { pub lp_paused_operations: u8, pub lp_exchange_fee_excluscion_scalar: u8, pub last_fill_price: u64, - pub padding: [u8; 24], + pub lp_pool_id: u8, + pub padding: [u8; 23], } impl Default for PerpMarket { @@ -298,7 +299,8 @@ impl Default for PerpMarket { lp_exchange_fee_excluscion_scalar: 0, lp_paused_operations: 0, last_fill_price: 0, - padding: [0; 24], + lp_pool_id: 0, + padding: [0; 23], } } } diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 43070e6dcf..13d7bde79f 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -427,12 +427,12 @@ export function getRevenueShareEscrowAccountPublicKey( export function getLpPoolPublicKey( programId: PublicKey, - nameBuffer: number[] + lpPoolId: number ): PublicKey { return PublicKey.findProgramAddressSync( [ Buffer.from(anchor.utils.bytes.utf8.encode('lp_pool')), - Buffer.from(nameBuffer), + new anchor.BN(lpPoolId).toArrayLike(Buffer, 'le', 1), ], programId )[0]; diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 9f345da8d1..d3e8b39b7b 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -505,7 +505,8 @@ export class AdminClient extends DriftClient { concentrationCoefScale = ONE, curveUpdateIntensity = 0, ammJitIntensity = 0, - name = DEFAULT_MARKET_NAME + name = DEFAULT_MARKET_NAME, + lpPoolId: number = 0 ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; @@ -542,7 +543,8 @@ export class AdminClient extends DriftClient { curveUpdateIntensity, ammJitIntensity, name, - mustInitializeAmmCache + mustInitializeAmmCache, + lpPoolId ); const tx = await this.buildTransaction(initializeMarketIxs); @@ -589,7 +591,8 @@ export class AdminClient extends DriftClient { curveUpdateIntensity = 0, ammJitIntensity = 0, name = DEFAULT_MARKET_NAME, - includeInitAmmCacheIx = false + includeInitAmmCacheIx = false, + lpPoolId: number = 0 ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, @@ -628,6 +631,7 @@ export class AdminClient extends DriftClient { curveUpdateIntensity, ammJitIntensity, nameBuffer, + lpPoolId, { accounts: { state: await this.getStatePublicKey(), @@ -5231,7 +5235,7 @@ export class AdminClient extends DriftClient { } public async initializeLpPool( - name: string, + lpPoolId: number, minMintFee: BN, maxAum: BN, maxSettleQuoteAmountPerMarket: BN, @@ -5239,7 +5243,7 @@ export class AdminClient extends DriftClient { whitelistMint?: PublicKey ): Promise { const ixs = await this.getInitializeLpPoolIx( - name, + lpPoolId, minMintFee, maxAum, maxSettleQuoteAmountPerMarket, @@ -5252,14 +5256,14 @@ export class AdminClient extends DriftClient { } public async getInitializeLpPoolIx( - name: string, + lpPoolId: number, minMintFee: BN, maxAum: BN, maxSettleQuoteAmountPerMarket: BN, mint: Keypair, whitelistMint?: PublicKey ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, encodeName(name)); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const ammConstituentMapping = getAmmConstituentMappingPublicKey( this.program.programId, lpPool @@ -5292,7 +5296,7 @@ export class AdminClient extends DriftClient { createMintAccountIx, createMintIx, this.program.instruction.initializeLpPool( - encodeName(name), + lpPoolId, minMintFee, maxAum, maxSettleQuoteAmountPerMarket, @@ -5324,11 +5328,11 @@ export class AdminClient extends DriftClient { } public async initializeConstituent( - lpPoolName: number[], + lpPoolId: number, initializeConstituentParams: InitializeConstituentParams ): Promise { const ixs = await this.getInitializeConstituentIx( - lpPoolName, + lpPoolId, initializeConstituentParams ); const tx = await this.buildTransaction(ixs); @@ -5337,10 +5341,10 @@ export class AdminClient extends DriftClient { } public async getInitializeConstituentIx( - lpPoolName: number[], + lpPoolId: number, initializeConstituentParams: InitializeConstituentParams ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const spotMarketIndex = initializeConstituentParams.spotMarketIndex; const constituentTargetBase = getConstituentTargetBasePublicKey( this.program.programId, @@ -5483,7 +5487,7 @@ export class AdminClient extends DriftClient { } public async updateConstituentParams( - lpPoolName: number[], + lpPoolId: number, constituentPublicKey: PublicKey, updateConstituentParams: { maxWeightDeviation?: BN; @@ -5501,7 +5505,7 @@ export class AdminClient extends DriftClient { } ): Promise { const ixs = await this.getUpdateConstituentParamsIx( - lpPoolName, + lpPoolId, constituentPublicKey, updateConstituentParams ); @@ -5511,7 +5515,7 @@ export class AdminClient extends DriftClient { } public async getUpdateConstituentParamsIx( - lpPoolName: number[], + lpPoolId: number, constituentPublicKey: PublicKey, updateConstituentParams: { maxWeightDeviation?: BN; @@ -5527,7 +5531,7 @@ export class AdminClient extends DriftClient { xi?: number; } ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); return [ this.program.instruction.updateConstituentParams( Object.assign( @@ -5566,7 +5570,7 @@ export class AdminClient extends DriftClient { } public async updateLpPoolParams( - lpPoolName: number[], + lpPoolId: number, updateLpPoolParams: { maxSettleQuoteAmount?: BN; volatility?: BN; @@ -5577,7 +5581,7 @@ export class AdminClient extends DriftClient { } ): Promise { const ixs = await this.getUpdateLpPoolParamsIx( - lpPoolName, + lpPoolId, updateLpPoolParams ); const tx = await this.buildTransaction(ixs); @@ -5586,7 +5590,7 @@ export class AdminClient extends DriftClient { } public async getUpdateLpPoolParamsIx( - lpPoolName: number[], + lpPoolId: number, updateLpPoolParams: { maxSettleQuoteAmount?: BN; volatility?: BN; @@ -5596,7 +5600,7 @@ export class AdminClient extends DriftClient { maxAum?: BN; } ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); return [ this.program.instruction.updateLpPoolParams( Object.assign( @@ -5623,11 +5627,11 @@ export class AdminClient extends DriftClient { } public async addAmmConstituentMappingData( - lpPoolName: number[], + lpPoolId: number, addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] ): Promise { const ixs = await this.getAddAmmConstituentMappingDataIx( - lpPoolName, + lpPoolId, addAmmConstituentMappingData ); const tx = await this.buildTransaction(ixs); @@ -5636,10 +5640,10 @@ export class AdminClient extends DriftClient { } public async getAddAmmConstituentMappingDataIx( - lpPoolName: number[], + lpPoolId: number, addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const ammConstituentMapping = getAmmConstituentMappingPublicKey( this.program.programId, lpPool @@ -5667,11 +5671,11 @@ export class AdminClient extends DriftClient { } public async updateAmmConstituentMappingData( - lpPoolName: number[], + lpPoolId: number, addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] ): Promise { const ixs = await this.getUpdateAmmConstituentMappingDataIx( - lpPoolName, + lpPoolId, addAmmConstituentMappingData ); const tx = await this.buildTransaction(ixs); @@ -5680,10 +5684,10 @@ export class AdminClient extends DriftClient { } public async getUpdateAmmConstituentMappingDataIx( - lpPoolName: number[], + lpPoolId: number, addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const ammConstituentMapping = getAmmConstituentMappingPublicKey( this.program.programId, lpPool @@ -5705,12 +5709,12 @@ export class AdminClient extends DriftClient { } public async removeAmmConstituentMappingData( - lpPoolName: number[], + lpPoolId: number, perpMarketIndex: number, constituentIndex: number ): Promise { const ixs = await this.getRemoveAmmConstituentMappingDataIx( - lpPoolName, + lpPoolId, perpMarketIndex, constituentIndex ); @@ -5720,11 +5724,11 @@ export class AdminClient extends DriftClient { } public async getRemoveAmmConstituentMappingDataIx( - lpPoolName: number[], + lpPoolId: number, perpMarketIndex: number, constituentIndex: number ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const ammConstituentMapping = getAmmConstituentMappingPublicKey( this.program.programId, lpPool @@ -5748,13 +5752,13 @@ export class AdminClient extends DriftClient { } public async updateConstituentCorrelationData( - lpPoolName: number[], + lpPoolId: number, index1: number, index2: number, correlation: BN ): Promise { const ixs = await this.getUpdateConstituentCorrelationDataIx( - lpPoolName, + lpPoolId, index1, index2, correlation @@ -5765,12 +5769,12 @@ export class AdminClient extends DriftClient { } public async getUpdateConstituentCorrelationDataIx( - lpPoolName: number[], + lpPoolId: number, index1: number, index2: number, correlation: BN ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); return [ this.program.instruction.updateConstituentCorrelationData( index1, @@ -5805,7 +5809,7 @@ export class AdminClient extends DriftClient { */ public async getSwapIx( { - lpPoolName, + lpPoolId, outMarketIndex, inMarketIndex, amountIn, @@ -5815,7 +5819,7 @@ export class AdminClient extends DriftClient { reduceOnly, userAccountPublicKey, }: { - lpPoolName: number[]; + lpPoolId: number; outMarketIndex: number; inMarketIndex: number; amountIn: BN; @@ -5848,7 +5852,7 @@ export class AdminClient extends DriftClient { const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const outConstituent = getConstituentPublicKey( this.program.programId, lpPool, @@ -5932,7 +5936,7 @@ export class AdminClient extends DriftClient { swapMode, onlyDirectRoutes, quote, - lpPoolName, + lpPoolId, }: { jupiterClient: JupiterClient; outMarketIndex: number; @@ -5944,7 +5948,7 @@ export class AdminClient extends DriftClient { swapMode?: SwapMode; onlyDirectRoutes?: boolean; quote?: QuoteResponse; - lpPoolName: number[]; + lpPoolId: number; }): Promise<{ ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; @@ -6036,7 +6040,7 @@ export class AdminClient extends DriftClient { } const { beginSwapIx, endSwapIx } = await this.getSwapIx({ - lpPoolName, + lpPoolId, outMarketIndex, inMarketIndex, amountIn: isExactOut ? exactOutBufferedAmountIn : amountIn, @@ -6125,7 +6129,7 @@ export class AdminClient extends DriftClient { } public async getAllDevnetLpSwapIxs( - lpPoolName: number[], + lpPoolId: number, inMarketIndex: number, outMarketIndex: number, inAmount: BN, @@ -6134,7 +6138,7 @@ export class AdminClient extends DriftClient { ) { const { beginSwapIx, endSwapIx } = await this.getSwapIx( { - lpPoolName, + lpPoolId, inMarketIndex, outMarketIndex, amountIn: inAmount, @@ -6178,7 +6182,7 @@ export class AdminClient extends DriftClient { } public async depositWithdrawToProgramVault( - lpPoolName: number[], + lpPoolId: number, depositMarketIndex: number, borrowMarketIndex: number, amountToDeposit: BN, @@ -6186,7 +6190,7 @@ export class AdminClient extends DriftClient { ): Promise { const { depositIx, withdrawIx } = await this.getDepositWithdrawToProgramVaultIxs( - lpPoolName, + lpPoolId, depositMarketIndex, borrowMarketIndex, amountToDeposit, @@ -6199,7 +6203,7 @@ export class AdminClient extends DriftClient { } public async getDepositWithdrawToProgramVaultIxs( - lpPoolName: number[], + lpPoolId: number, depositMarketIndex: number, borrowMarketIndex: number, amountToDeposit: BN, @@ -6208,7 +6212,7 @@ export class AdminClient extends DriftClient { depositIx: TransactionInstruction; withdrawIx: TransactionInstruction; }> { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const depositSpotMarket = this.getSpotMarketAccount(depositMarketIndex); const withdrawSpotMarket = this.getSpotMarketAccount(borrowMarketIndex); @@ -6278,12 +6282,12 @@ export class AdminClient extends DriftClient { } public async depositToProgramVault( - lpPoolName: number[], + lpPoolId: number, depositMarketIndex: number, amountToDeposit: BN ): Promise { const depositIx = await this.getDepositToProgramVaultIx( - lpPoolName, + lpPoolId, depositMarketIndex, amountToDeposit ); @@ -6294,12 +6298,12 @@ export class AdminClient extends DriftClient { } public async withdrawFromProgramVault( - lpPoolName: number[], + lpPoolId: number, borrowMarketIndex: number, amountToWithdraw: BN ): Promise { const withdrawIx = await this.getWithdrawFromProgramVaultIx( - lpPoolName, + lpPoolId, borrowMarketIndex, amountToWithdraw ); @@ -6309,12 +6313,12 @@ export class AdminClient extends DriftClient { } public async getDepositToProgramVaultIx( - lpPoolName: number[], + lpPoolId: number, depositMarketIndex: number, amountToDeposit: BN ): Promise { const { depositIx } = await this.getDepositWithdrawToProgramVaultIxs( - lpPoolName, + lpPoolId, depositMarketIndex, depositMarketIndex, amountToDeposit, @@ -6324,12 +6328,12 @@ export class AdminClient extends DriftClient { } public async getWithdrawFromProgramVaultIx( - lpPoolName: number[], + lpPoolId: number, borrowMarketIndex: number, amountToWithdraw: BN ): Promise { const { withdrawIx } = await this.getDepositWithdrawToProgramVaultIxs( - lpPoolName, + lpPoolId, borrowMarketIndex, borrowMarketIndex, new BN(0), diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts index e4ab519944..4ac41a4357 100644 --- a/sdk/src/constituentMap/constituentMap.ts +++ b/sdk/src/constituentMap/constituentMap.ts @@ -17,8 +17,6 @@ import { getLpPoolPublicKey } from '../addresses/pda'; const MAX_CONSTITUENT_SIZE_BYTES = 320; // TODO: update this when account is finalized -const LP_POOL_NAME = 'test lp pool 3'; - export type ConstituentMapConfig = { driftClient: DriftClient; connection?: Connection; @@ -34,7 +32,7 @@ export type ConstituentMapConfig = { logResubMessages?: boolean; commitment?: Commitment; }; - lpPoolName?: string; + lpPoolId?: number; // potentially use these to filter Constituent accounts additionalFilters?: MemcmpFilter[]; }; @@ -67,14 +65,14 @@ export class ConstituentMap implements ConstituentMapInterface { private constituentIndexToKeyMap = new Map(); private spotMarketIndexToKeyMap = new Map(); - private lpPoolName: string; + private lpPoolId: number; constructor(config: ConstituentMapConfig) { this.driftClient = config.driftClient; this.additionalFilters = config.additionalFilters; this.commitment = config.subscriptionConfig.commitment; this.connection = config.connection || this.driftClient.connection; - this.lpPoolName = config.lpPoolName ?? LP_POOL_NAME; + this.lpPoolId = config.lpPoolId ?? 0; if (config.subscriptionConfig.type === 'polling') { this.constituentAccountSubscriber = @@ -109,10 +107,7 @@ export class ConstituentMap implements ConstituentMapInterface { const filters = [ getConstituentFilter(), getConstituentLpPoolFilter( - getLpPoolPublicKey( - this.driftClient.program.programId, - encodeName(this.lpPoolName) - ) + getLpPoolPublicKey(this.driftClient.program.programId, this.lpPoolId) ), ]; if (this.additionalFilters) { diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index f4fd50e7a5..7c3f79a9f2 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -10938,19 +10938,19 @@ export class DriftClient { }); } - public async getLpPoolAccount(lpPoolName: number[]): Promise { + public async getLpPoolAccount(lpPoolId: number): Promise { return (await this.program.account.lpPool.fetch( - getLpPoolPublicKey(this.program.programId, lpPoolName) + getLpPoolPublicKey(this.program.programId, lpPoolId) )) as LPPoolAccount; } public async getConstituentTargetBaseAccount( - lpPoolName: number[] + lpPoolId: number ): Promise { return (await this.program.account.constituentTargetBase.fetch( getConstituentTargetBasePublicKey( this.program.programId, - getLpPoolPublicKey(this.program.programId, lpPoolName) + getLpPoolPublicKey(this.program.programId, lpPoolId) ) )) as ConstituentTargetBaseAccount; } @@ -10962,13 +10962,13 @@ export class DriftClient { } public async updateLpConstituentTargetBase( - lpPoolName: number[], + lpPoolId: number, constituents: PublicKey[], txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( - await this.getUpdateLpConstituentTargetBaseIx(lpPoolName, constituents), + await this.getUpdateLpConstituentTargetBaseIx(lpPoolId, constituents), txParams ), [], @@ -10978,10 +10978,10 @@ export class DriftClient { } public async getUpdateLpConstituentTargetBaseIx( - lpPoolName: number[], + lpPoolId: number, constituents: PublicKey[] ): Promise { - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); const ammConstituentMappingPublicKey = getAmmConstituentMappingPublicKey( this.program.programId, lpPool @@ -11824,7 +11824,7 @@ export class DriftClient { const ixs: TransactionInstruction[] = []; ixs.push( ...(await this.getAllSettlePerpToLpPoolIxs( - lpPool.name, + lpPool.lpPoolId, this.getPerpMarketAccounts() .filter((marketAccount) => marketAccount.lpStatus > 0) .map((marketAccount) => marketAccount.marketIndex) @@ -11904,7 +11904,7 @@ export class DriftClient { ixs.push( await this.getUpdateLpConstituentTargetBaseIx( - lpPool.name, + lpPool.lpPoolId, Array.from(constituentMap.values()).map( (constituent) => constituent.pubkey ) @@ -11943,12 +11943,12 @@ export class DriftClient { } async settlePerpToLpPool( - lpPoolName: number[], + lpPoolId: number, perpMarketIndexes: number[] ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( - await this.getSettlePerpToLpPoolIx(lpPoolName, perpMarketIndexes), + await this.getSettlePerpToLpPoolIx(lpPoolId, perpMarketIndexes), undefined ), [], @@ -11958,7 +11958,7 @@ export class DriftClient { } public async getSettlePerpToLpPoolIx( - lpPoolName: number[], + lpPoolId: number, perpMarketIndexes: number[] ): Promise { const remainingAccounts = []; @@ -11972,7 +11972,7 @@ export class DriftClient { }) ); const quoteSpotMarketAccount = this.getQuoteSpotMarketAccount(); - const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); return this.program.instruction.settlePerpToLpPool({ accounts: { driftSigner: this.getSignerPublicKey(), @@ -11995,12 +11995,12 @@ export class DriftClient { } public async getAllSettlePerpToLpPoolIxs( - lpPoolName: number[], + lpPoolId: number, marketIndexes: number[] ): Promise { const ixs: TransactionInstruction[] = []; ixs.push(await this.getUpdateAmmCacheIx(marketIndexes)); - ixs.push(await this.getSettlePerpToLpPoolIx(lpPoolName, marketIndexes)); + ixs.push(await this.getSettlePerpToLpPoolIx(lpPoolId, marketIndexes)); return ixs; } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index c5af37ac4a..e402e22c48 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4415,6 +4415,10 @@ 32 ] } + }, + { + "name": "lpPoolId", + "type": "u8" } ] }, @@ -5233,6 +5237,32 @@ } ] }, + { + "name": "updatePerpMarketLpPoolId", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolId", + "type": "u8" + } + ] + }, { "name": "updateInsuranceFundUnstakingPeriod", "accounts": [ @@ -7346,13 +7376,8 @@ ], "args": [ { - "name": "name", - "type": { - "array": [ - "u8", - 32 - ] - } + "name": "lpPoolId", + "type": "u8" }, { "name": "minMintFee", @@ -9755,18 +9780,6 @@ "type": { "kind": "struct", "fields": [ - { - "name": "name", - "docs": [ - "name of vault, TODO: check type + size" - ], - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "pubkey", "docs": [ @@ -9897,12 +9910,16 @@ "name": "targetPositionDelayFeeBpsPer10Slots", "type": "u8" }, + { + "name": "lpPoolId", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 15 + 14 ] } } @@ -10533,12 +10550,16 @@ "name": "lastFillPrice", "type": "u64" }, + { + "name": "lpPoolId", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 24 + 23 ] } } @@ -19653,96 +19674,81 @@ }, { "code": 6327, - "name": "InvalidUpdateConstituentTargetBaseArgument", - "msg": "Invalid update constituent update target weights argument" - }, - { - "code": 6328, "name": "ConstituentNotFound", "msg": "Constituent not found" }, { - "code": 6329, + "code": 6328, "name": "ConstituentCouldNotLoad", "msg": "Constituent could not load" }, { - "code": 6330, + "code": 6329, "name": "ConstituentWrongMutability", "msg": "Constituent wrong mutability" }, { - "code": 6331, + "code": 6330, "name": "WrongNumberOfConstituents", "msg": "Wrong number of constituents passed to instruction" }, { - "code": 6332, - "name": "OracleTooStaleForLPAUMUpdate", - "msg": "Oracle too stale for LP AUM update" - }, - { - "code": 6333, + "code": 6331, "name": "InsufficientConstituentTokenBalance", "msg": "Insufficient constituent token balance" }, { - "code": 6334, + "code": 6332, "name": "AMMCacheStale", "msg": "Amm Cache data too stale" }, { - "code": 6335, + "code": 6333, "name": "LpPoolAumDelayed", "msg": "LP Pool AUM not updated recently" }, { - "code": 6336, + "code": 6334, "name": "ConstituentOracleStale", "msg": "Constituent oracle is stale" }, { - "code": 6337, + "code": 6335, "name": "LpInvariantFailed", "msg": "LP Invariant failed" }, { - "code": 6338, + "code": 6336, "name": "InvalidConstituentDerivativeWeights", "msg": "Invalid constituent derivative weights" }, { - "code": 6339, - "name": "UnauthorizedDlpAuthority", - "msg": "Unauthorized dlp authority" - }, - { - "code": 6340, + "code": 6337, "name": "MaxDlpAumBreached", "msg": "Max DLP AUM Breached" }, { - "code": 6341, + "code": 6338, "name": "SettleLpPoolDisabled", "msg": "Settle Lp Pool Disabled" }, { - "code": 6342, + "code": 6339, "name": "MintRedeemLpPoolDisabled", "msg": "Mint/Redeem Lp Pool Disabled" }, { - "code": 6343, + "code": 6340, "name": "LpPoolSettleInvariantBreached", "msg": "Settlement amount exceeded" }, { - "code": 6344, + "code": 6341, "name": "InvalidConstituentOperation", "msg": "Invalid constituent operation" }, { - "code": 6345, + "code": 6342, "name": "Unauthorized", "msg": "Unauthorized for operation" } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index f9375ed781..e506c57655 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -895,6 +895,7 @@ export type PerpMarketAccount = { protectedMakerDynamicDivisor: number; lastFillPrice: BN; + lpPoolId: number; lpFeeTransferScalar: number; lpExchangeFeeExcluscionScalar: number; lpStatus: number; @@ -1769,7 +1770,7 @@ export type ConstituentCorrelations = { }; export type LPPoolAccount = { - name: number[]; + lpPoolId: number; pubkey: PublicKey; mint: PublicKey; whitelistMint: PublicKey; diff --git a/tests/lpPool.ts b/tests/lpPool.ts index c0b6867659..fffd5c9e58 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -114,12 +114,9 @@ describe('LP Pool', () => { let solUsd: PublicKey; let solUsdLazer: PublicKey; - const lpPoolName = 'test pool 1'; + const lpPoolId = 0; const tokenDecimals = 6; - const lpPoolKey = getLpPoolPublicKey( - program.programId, - encodeName(lpPoolName) - ); + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); let whitelistMint: PublicKey; @@ -268,7 +265,7 @@ describe('LP Pool', () => { ); await adminClient.initializeLpPool( - lpPoolName, + lpPoolId, ZERO, new BN(1_000_000_000_000).mul(QUOTE_PRECISION), new BN(1_000_000).mul(QUOTE_PRECISION), @@ -371,7 +368,7 @@ describe('LP Pool', () => { }); it('can add constituents to LP Pool', async () => { - await adminClient.initializeConstituent(encodeName(lpPoolName), { + await adminClient.initializeConstituent(lpPoolId, { spotMarketIndex: 0, decimals: 6, maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), @@ -421,7 +418,7 @@ describe('LP Pool', () => { expect(constituentTokenVault).to.not.be.null; // Add second constituent representing SOL - await adminClient.initializeConstituent(lpPool.name, { + await adminClient.initializeConstituent(lpPool.lpPoolId, { spotMarketIndex: 1, decimals: 6, maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), @@ -441,7 +438,7 @@ describe('LP Pool', () => { it('can add amm mapping datum', async () => { // Firt constituent is USDC, so add no mapping. We will add a second mapping though // for the second constituent which is SOL - await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + await adminClient.addAmmConstituentMappingData(lpPoolId, [ { perpMarketIndex: 1, constituentIndex: 1, @@ -461,7 +458,7 @@ describe('LP Pool', () => { }); it('can update and remove amm constituent mapping entries', async () => { - await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + await adminClient.addAmmConstituentMappingData(lpPoolId, [ { perpMarketIndex: 2, constituentIndex: 0, @@ -480,7 +477,7 @@ describe('LP Pool', () => { assert(ammMapping.weights.length == 2); // Update - await adminClient.updateAmmConstituentMappingData(encodeName(lpPoolName), [ + await adminClient.updateAmmConstituentMappingData(lpPoolId, [ { perpMarketIndex: 2, constituentIndex: 0, @@ -498,11 +495,7 @@ describe('LP Pool', () => { ); // Remove - await adminClient.removeAmmConstituentMappingData( - encodeName(lpPoolName), - 2, - 0 - ); + await adminClient.removeAmmConstituentMappingData(lpPoolId, 2, 0); ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( ammConstituentMapping )) as AmmConstituentMapping; @@ -537,13 +530,9 @@ describe('LP Pool', () => { constituentPublicKey )) as ConstituentAccount; - await adminClient.updateConstituentParams( - encodeName(lpPoolName), - constituentPublicKey, - { - costToTradeBps: 10, - } - ); + await adminClient.updateConstituentParams(lpPoolId, constituentPublicKey, { + costToTradeBps: 10, + }); const constituentTargetBase = getConstituentTargetBasePublicKey( program.programId, lpPoolKey @@ -556,14 +545,14 @@ describe('LP Pool', () => { assert(targets.targets[constituent.constituentIndex].costToTradeBps == 10); await adminClient.updateConstituentCorrelationData( - encodeName(lpPoolName), + lpPoolId, 0, 1, PERCENTAGE_PRECISION.muln(87).divn(100) ); await adminClient.updateConstituentCorrelationData( - encodeName(lpPoolName), + lpPoolId, 0, 1, PERCENTAGE_PRECISION @@ -573,7 +562,7 @@ describe('LP Pool', () => { it('fails adding datum with bad params', async () => { // Bad perp market index try { - await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + await adminClient.addAmmConstituentMappingData(lpPoolId, [ { perpMarketIndex: 3, constituentIndex: 0, @@ -588,7 +577,7 @@ describe('LP Pool', () => { // Bad constituent index try { - await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + await adminClient.addAmmConstituentMappingData(lpPoolId, [ { perpMarketIndex: 0, constituentIndex: 5, @@ -740,13 +729,10 @@ describe('LP Pool', () => { let tx = new Transaction(); tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); tx.add( - await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), - [ - getConstituentPublicKey(program.programId, lpPoolKey, 0), - getConstituentPublicKey(program.programId, lpPoolKey, 1), - ] - ) + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ]) ); await adminClient.sendTransaction(tx); @@ -773,13 +759,10 @@ describe('LP Pool', () => { tx = new Transaction(); tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); tx.add( - await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), - [ - getConstituentPublicKey(program.programId, lpPoolKey, 0), - getConstituentPublicKey(program.programId, lpPoolKey, 1), - ] - ) + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ]) ); await adminClient.sendTransaction(tx); constituentTargetBase = @@ -801,7 +784,7 @@ describe('LP Pool', () => { lpPoolKey )) as LPPoolAccount; - await adminClient.initializeConstituent(lpPool.name, { + await adminClient.initializeConstituent(lpPool.lpPoolId, { spotMarketIndex: 2, decimals: 6, maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), @@ -834,14 +817,11 @@ describe('LP Pool', () => { const tx = new Transaction(); tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( - await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), - [ - getConstituentPublicKey(program.programId, lpPoolKey, 0), - getConstituentPublicKey(program.programId, lpPoolKey, 1), - getConstituentPublicKey(program.programId, lpPoolKey, 2), - ] - ) + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) ); await adminClient.sendTransaction(tx); @@ -879,14 +859,11 @@ describe('LP Pool', () => { tx2 .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) .add( - await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), - [ - getConstituentPublicKey(program.programId, lpPoolKey, 0), - getConstituentPublicKey(program.programId, lpPoolKey, 1), - getConstituentPublicKey(program.programId, lpPoolKey, 2), - ] - ) + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) ); await adminClient.sendTransaction(tx2); await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); @@ -915,14 +892,11 @@ describe('LP Pool', () => { tx3 .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) .add( - await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), - [ - getConstituentPublicKey(program.programId, lpPoolKey, 0), - getConstituentPublicKey(program.programId, lpPoolKey, 1), - getConstituentPublicKey(program.programId, lpPoolKey, 2), - ] - ) + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) ); await adminClient.sendTransaction(tx3); await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); @@ -1083,10 +1057,7 @@ describe('LP Pool', () => { const settleTx = new Transaction(); settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); settleTx.add( - await adminClient.getSettlePerpToLpPoolIx( - encodeName(lpPoolName), - [0, 1, 2] - ) + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) ); settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); await adminClient.sendTransaction(settleTx); @@ -1164,7 +1135,10 @@ describe('LP Pool', () => { const tx = new Transaction(); tx.add( - ...(await adminClient.getAllSettlePerpToLpPoolIxs(lpPool.name, [0, 1, 2])) + ...(await adminClient.getAllSettlePerpToLpPoolIxs( + lpPool.lpPoolId, + [0, 1, 2] + )) ); tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); tx.add( @@ -1220,10 +1194,7 @@ describe('LP Pool', () => { settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); settleTx.add( - await adminClient.getSettlePerpToLpPoolIx( - encodeName(lpPoolName), - [0, 1, 2] - ) + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) ); await adminClient.sendTransaction(settleTx); @@ -1299,10 +1270,7 @@ describe('LP Pool', () => { const settleTx = new Transaction(); settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); settleTx.add( - await adminClient.getSettlePerpToLpPoolIx( - encodeName(lpPoolName), - [0, 1, 2] - ) + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) ); await adminClient.sendTransaction(settleTx); @@ -1403,10 +1371,7 @@ describe('LP Pool', () => { const settleTx = new Transaction(); settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); settleTx.add( - await adminClient.getSettlePerpToLpPoolIx( - encodeName(lpPoolName), - [0, 1, 2] - ) + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) ); settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); await adminClient.sendTransaction(settleTx); @@ -1432,7 +1397,7 @@ describe('LP Pool', () => { lpPoolKey )) as LPPoolAccount; - await adminClient.initializeConstituent(lpPool.name, { + await adminClient.initializeConstituent(lpPool.lpPoolId, { spotMarketIndex: 3, decimals: 6, maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), @@ -1454,7 +1419,7 @@ describe('LP Pool', () => { }); await adminClient.updateConstituentParams( - lpPool.name, + lpPool.lpPoolId, getConstituentPublicKey(program.programId, lpPoolKey, 2), { derivativeWeight: PERCENTAGE_PRECISION.divn(4), @@ -1477,15 +1442,12 @@ describe('LP Pool', () => { const tx = new Transaction(); tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( - await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), - [ - getConstituentPublicKey(program.programId, lpPoolKey, 0), - getConstituentPublicKey(program.programId, lpPoolKey, 1), - getConstituentPublicKey(program.programId, lpPoolKey, 2), - getConstituentPublicKey(program.programId, lpPoolKey, 3), - ] - ) + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ]) ); await adminClient.sendTransaction(tx); await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); @@ -1519,7 +1481,7 @@ describe('LP Pool', () => { // Set the derivative weights to 0 await adminClient.updateConstituentParams( - lpPool.name, + lpPool.lpPoolId, getConstituentPublicKey(program.programId, lpPoolKey, 2), { derivativeWeight: ZERO, @@ -1527,7 +1489,7 @@ describe('LP Pool', () => { ); await adminClient.updateConstituentParams( - lpPool.name, + lpPool.lpPoolId, getConstituentPublicKey(program.programId, lpPoolKey, 3), { derivativeWeight: ZERO, @@ -1539,15 +1501,12 @@ describe('LP Pool', () => { tx2 .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) .add( - await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), - [ - getConstituentPublicKey(program.programId, lpPoolKey, 0), - getConstituentPublicKey(program.programId, lpPoolKey, 1), - getConstituentPublicKey(program.programId, lpPoolKey, 2), - getConstituentPublicKey(program.programId, lpPoolKey, 3), - ] - ) + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ]) ); await adminClient.sendTransaction(tx2); await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); @@ -1571,7 +1530,7 @@ describe('LP Pool', () => { it('cant withdraw more than constituent limit', async () => { await adminClient.updateConstituentParams( - encodeName(lpPoolName), + lpPoolId, getConstituentPublicKey(program.programId, lpPoolKey, 0), { maxBorrowTokenAmount: new BN(10).muln(10 ** 6), @@ -1580,7 +1539,7 @@ describe('LP Pool', () => { try { await adminClient.withdrawFromProgramVault( - encodeName(lpPoolName), + lpPoolId, 0, new BN(100).mul(QUOTE_PRECISION) ); @@ -1594,7 +1553,7 @@ describe('LP Pool', () => { await adminClient.updateFeatureBitFlagsSettleLpPool(false); try { - await adminClient.settlePerpToLpPool(encodeName(lpPoolName), [0, 1, 2]); + await adminClient.settlePerpToLpPool(lpPoolId, [0, 1, 2]); assert(false, 'Should have thrown'); } catch (e) { console.log(e.message); @@ -1613,7 +1572,7 @@ describe('LP Pool', () => { new BN(7_000).mul(new BN(10 ** 9)) ); await adminClient.deposit(new BN(1000).mul(new BN(10 ** 9)), 2, pubkey, 1); - const lpPool = await adminClient.getLpPoolAccount(encodeName(lpPoolName)); + const lpPool = await adminClient.getLpPoolAccount(lpPoolId); // Deposit into LP pool some balance const ixs = []; @@ -1628,7 +1587,7 @@ describe('LP Pool', () => { ); await adminClient.sendTransaction(new Transaction().add(...ixs)); await adminClient.depositToProgramVault( - lpPool.name, + lpPool.lpPoolId, 2, new BN(100).mul(new BN(10 ** 9)) ); @@ -1658,18 +1617,18 @@ describe('LP Pool', () => { // ); await adminClient.withdrawFromProgramVault( - encodeName(lpPoolName), + lpPoolId, 2, new BN(500).mul(new BN(10 ** 9)) ); }); it('whitelist mint', async () => { - await adminClient.updateLpPoolParams(encodeName(lpPoolName), { + await adminClient.updateLpPoolParams(lpPoolId, { whitelistMint: whitelistMint, }); - const lpPool = await adminClient.getLpPoolAccount(encodeName(lpPoolName)); + const lpPool = await adminClient.getLpPoolAccount(lpPoolId); assert(lpPool.whitelistMint.equals(whitelistMint)); console.log('lpPool.whitelistMint', lpPool.whitelistMint.toString()); @@ -1725,4 +1684,51 @@ describe('LP Pool', () => { // successfully call add liquidity await adminClient.sendTransaction(txAfter); }); + + it('can initialize multiple lp pools', async () => { + const newLpPoolId = 1; + const newLpPoolKey = getLpPoolPublicKey(program.programId, newLpPoolId); + + await adminClient.initializeLpPool( + newLpPoolId, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() + ); + await adminClient.initializeConstituent(newLpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(2), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [], + constituentDerivativeIndex: -1, + }); + + const oldLpPool = await adminClient.getLpPoolAccount(lpPoolId); + // cant settle a perp market to the new lp pool with a different id + + try { + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(newLpPoolId, [0, 1, 2]) + ); + settleTx.add( + await adminClient.getUpdateLpPoolAumIxs(oldLpPool, [0, 1, 2]) + ); + await adminClient.sendTransaction(settleTx); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c7')); + } + }); }); diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index adbf86979e..544cee85b2 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -124,12 +124,9 @@ describe('LP Pool', () => { ); let solUsd: PublicKey; - const lpPoolName = 'test pool 1'; + const lpPoolId = 0; const tokenDecimals = 6; - const lpPoolKey = getLpPoolPublicKey( - program.programId, - encodeName(lpPoolName) - ); + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( new BN(2) @@ -252,7 +249,7 @@ describe('LP Pool', () => { it('can create a new LP Pool', async () => { await adminClient.initializeLpPool( - lpPoolName, + lpPoolId, ZERO, new BN(1_000_000_000_000).mul(QUOTE_PRECISION), new BN(1_000_000).mul(QUOTE_PRECISION), @@ -308,7 +305,7 @@ describe('LP Pool', () => { it('can add constituents to LP Pool', async () => { // USDC Constituent - await adminClient.initializeConstituent(encodeName(lpPoolName), { + await adminClient.initializeConstituent(lpPoolId, { spotMarketIndex: 0, decimals: 6, maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), @@ -362,7 +359,7 @@ describe('LP Pool', () => { const correlations = [ZERO]; for (let i = 1; i < NUMBER_OF_CONSTITUENTS; i++) { - await adminClient.initializeConstituent(encodeName(lpPoolName), { + await adminClient.initializeConstituent(lpPoolId, { spotMarketIndex: i, decimals: 6, maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), @@ -507,7 +504,7 @@ describe('LP Pool', () => { // Assume that constituent 0 is USDC for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { for (let j = 1; j <= 3; j++) { - await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + await adminClient.addAmmConstituentMappingData(lpPoolId, [ { perpMarketIndex: i, constituentIndex: j, @@ -599,7 +596,7 @@ describe('LP Pool', () => { ) ); const updateBaseIx = await adminClient.getUpdateLpConstituentTargetBaseIx( - encodeName(lpPoolName), + lpPoolId, [getConstituentPublicKey(program.programId, lpPoolKey, 1)] ); diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 4c2aa62cfc..ae28ac8594 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -91,12 +91,9 @@ describe('LP Pool', () => { mantissaSqrtScale ); - const lpPoolName = 'test pool 1'; + const lpPoolId = 0; const tokenDecimals = 6; - const lpPoolKey = getLpPoolPublicKey( - program.programId, - encodeName(lpPoolName) - ); + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); let userUSDCAccount: Keypair; let serumMarket: Market; @@ -223,13 +220,13 @@ describe('LP Pool', () => { ); await adminClient.initializeLpPool( - lpPoolName, + lpPoolId, new BN(100), // 1 bps new BN(100_000_000).mul(QUOTE_PRECISION), new BN(1_000_000).mul(QUOTE_PRECISION), Keypair.generate() // dlp mint ); - await adminClient.initializeConstituent(encodeName(lpPoolName), { + await adminClient.initializeConstituent(lpPoolId, { spotMarketIndex: 0, decimals: 6, maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, @@ -244,7 +241,7 @@ describe('LP Pool', () => { }); await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); - await adminClient.initializeConstituent(encodeName(lpPoolName), { + await adminClient.initializeConstituent(lpPoolId, { spotMarketIndex: 1, decimals: 6, maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, @@ -641,7 +638,7 @@ describe('LP Pool', () => { removeTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); removeTx.add( await adminClient.getDepositToProgramVaultIx( - encodeName(lpPoolName), + lpPoolId, 0, new BN(constituentBalanceBefore) ) @@ -673,7 +670,7 @@ describe('LP Pool', () => { const withdrawFromProgramVaultTx = new Transaction(); withdrawFromProgramVaultTx.add( await adminClient.getWithdrawFromProgramVaultIx( - encodeName(lpPoolName), + lpPoolId, 0, blTokenAmountAfterRemoveLiquidity.abs() ) @@ -768,7 +765,7 @@ describe('LP Pool', () => { it('swap sol for usdc', async () => { // Initialize new constituent for market 2 - await adminClient.initializeConstituent(encodeName(lpPoolName), { + await adminClient.initializeConstituent(lpPoolId, { spotMarketIndex: 2, decimals: 6, maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, @@ -840,7 +837,7 @@ describe('LP Pool', () => { const { beginSwapIx, endSwapIx } = await adminClient.getSwapIx( { - lpPoolName: encodeName(lpPoolName), + lpPoolId: lpPoolId, amountIn: amountIn, inMarketIndex: 0, outMarketIndex: 2, @@ -952,7 +949,7 @@ describe('LP Pool', () => { ).amount.toString(); await adminClient.depositWithdrawToProgramVault( - encodeName(lpPoolName), + lpPoolId, 0, 2, new BN(400).mul(QUOTE_PRECISION), // 100 USDC From 8d3e848d8095d352786efc62d7b129854cacb736 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:43:36 -0700 Subject: [PATCH 146/159] address comments round 2 --- programs/drift/src/ids.rs | 9 + programs/drift/src/instructions/admin.rs | 21 +-- programs/drift/src/instructions/keeper.rs | 15 +- programs/drift/src/instructions/lp_admin.rs | 21 +-- programs/drift/src/instructions/user.rs | 17 +- programs/drift/src/lib.rs | 38 ++-- programs/drift/src/math/lp_pool.rs | 48 ++++- programs/drift/src/state/amm_cache.rs | 10 - programs/drift/src/state/lp_pool.rs | 96 ++++++++-- programs/drift/src/state/perp_market.rs | 2 +- programs/drift/src/state/perp_market/tests.rs | 2 +- sdk/src/adminClient.ts | 34 ++++ sdk/src/constituentMap/constituentMap.ts | 2 +- sdk/src/idl/drift.json | 178 +++++++++--------- tests/lpPool.ts | 1 + tests/lpPoolCUs.ts | 1 + tests/lpPoolSwap.ts | 9 + 17 files changed, 317 insertions(+), 187 deletions(-) diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index a8ccf3a06e..f7ac19adda 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -122,3 +122,12 @@ pub mod titan_mainnet_argos_v1 { use solana_program::declare_id; declare_id!("T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"); } + +pub const WHITELISTED_SWAP_PROGRAMS: &[solana_program::pubkey::Pubkey] = &[ + serum_program::id(), + jupiter_mainnet_3::id(), + jupiter_mainnet_4::id(), + jupiter_mainnet_6::id(), + dflow_mainnet_aggregator_4::id(), + titan_mainnet_argos_v1::id(), +]; diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index b260899694..f806412f02 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -3362,11 +3362,10 @@ pub fn handle_update_perp_market_paused_operations( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_contract_tier( - ctx: Context, + ctx: Context, contract_tier: ContractTier, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; - let amm_cache = &mut ctx.accounts.amm_cache; msg!("perp market {}", perp_market.market_index); msg!( @@ -3376,7 +3375,6 @@ pub fn handle_update_perp_market_contract_tier( ); perp_market.contract_tier = contract_tier; - amm_cache.update_perp_market_fields(perp_market)?; Ok(()) } @@ -5556,23 +5554,6 @@ pub struct HotAdminUpdatePerpMarket<'info> { pub perp_market: AccountLoader<'info, PerpMarket>, } -#[derive(Accounts)] -pub struct AdminUpdatePerpMarketContractTier<'info> { - pub admin: Signer<'info>, - #[account( - has_one = admin - )] - pub state: Box>, - #[account(mut)] - pub perp_market: AccountLoader<'info, PerpMarket>, - #[account( - mut, - seeds = [AMM_POSITIONS_CACHE.as_ref()], - bump = amm_cache.bump, - )] - pub amm_cache: Box>, -} - #[derive(Accounts)] pub struct AdminUpdatePerpMarketAmmSummaryStats<'info> { #[account( diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index e3fda5d45f..f0a09b992d 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -3313,7 +3313,6 @@ pub fn handle_pause_spot_market_deposit_withdraw( Ok(()) } -// Refactored main function pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, ) -> Result<()> { @@ -3402,6 +3401,8 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( return Err(ErrorCode::AMMCacheStale.into()); } + quote_constituent.sync_token_balance(ctx.accounts.constituent_quote_token_account.amount); + // Create settlement context let settlement_ctx = SettlementContext { quote_owed_from_lp: cached_info.quote_owed_from_lp_pool, @@ -3422,7 +3423,12 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( // Calculate settlement let settlement_result = calculate_settlement_amount(&settlement_ctx)?; - validate_settlement_amount(&settlement_ctx, &settlement_result)?; + validate_settlement_amount( + &settlement_ctx, + &settlement_result, + &perp_market, + quote_market, + )?; if settlement_result.direction == SettlementDirection::None { continue; @@ -3613,7 +3619,10 @@ pub struct SettleAmmPnlToLp<'info> { pub state: Box>, #[account(mut)] pub lp_pool: AccountLoader<'info, LPPool>, - #[account(mut)] + #[account( + mut, + constraint = keeper.key() == crate::ids::lp_pool_swap_wallet::id() || keeper.key() == admin_hot_wallet::id() || keeper.key() == state.admin.key(), + )] pub keeper: Signer<'info>, /// CHECK: checked in AmmCacheZeroCopy checks #[account(mut)] diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index bd475b1282..ccd117a72b 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1,6 +1,6 @@ use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; use crate::error::ErrorCode; -use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet}; +use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet, WHITELISTED_SWAP_PROGRAMS}; use crate::instructions::optional_accounts::{get_token_mint, load_maps, AccountMaps}; use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; use crate::math::safe_math::SafeMath; @@ -19,15 +19,11 @@ use crate::validate; use crate::{controller, load_mut}; use anchor_lang::prelude::*; use anchor_lang::Discriminator; -use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::Token; use anchor_spl::token_2022::Token2022; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; -use crate::ids::{ - jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, lighthouse, marinade_mainnet, - serum_program, -}; +use crate::ids::{lighthouse, marinade_mainnet}; use crate::state::traits::Size; use solana_program::sysvar::instructions; @@ -84,7 +80,7 @@ pub fn handle_initialize_lp_pool( target_oracle_delay_fee_bps_per_10_slots: 0, target_position_delay_fee_bps_per_10_slots: 0, lp_pool_id, - padding: [0u8; 14], + padding: [0u8; 174], whitelist_mint, }; @@ -664,9 +660,6 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( in_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_in_token_account.amount; out_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_out_token_account.amount; - // drop(in_constituent); - // drop(out_constituent); - send_from_program_vault_with_signature_seeds( &ctx.accounts.token_program, constituent_in_token_account, @@ -767,13 +760,7 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( )?; } } else { - let mut whitelisted_programs = vec![ - serum_program::id(), - AssociatedToken::id(), - jupiter_mainnet_3::ID, - jupiter_mainnet_4::ID, - jupiter_mainnet_6::ID, - ]; + let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); whitelisted_programs.push(Token::id()); whitelisted_programs.push(Token2022::id()); whitelisted_programs.push(marinade_mainnet::ID); diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index c489692bc6..ed95c6fe7f 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -23,10 +23,8 @@ use crate::controller::spot_position::{ use crate::error::ErrorCode; use crate::get_then_update_id; use crate::ids::admin_hot_wallet; -use crate::ids::{ - dflow_mainnet_aggregator_4, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, - lighthouse, marinade_mainnet, serum_program, titan_mainnet_argos_v1, -}; +use crate::ids::WHITELISTED_SWAP_PROGRAMS; +use crate::ids::{lighthouse, marinade_mainnet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{ @@ -117,7 +115,6 @@ use crate::validation::whitelist::validate_whitelist_token; use crate::{controller, math}; use crate::{load_mut, ExchangeStatus}; use anchor_lang::solana_program::sysvar::instructions; -use anchor_spl::associated_token::AssociatedToken; use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::sysvar::instructions::ID as IX_ID; @@ -3680,15 +3677,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( )?; } } else { - let mut whitelisted_programs = vec![ - serum_program::id(), - AssociatedToken::id(), - jupiter_mainnet_3::ID, - jupiter_mainnet_4::ID, - jupiter_mainnet_6::ID, - dflow_mainnet_aggregator_4::ID, - titan_mainnet_argos_v1::ID, - ]; + let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); if !delegate_is_signer { whitelisted_programs.push(Token::id()); whitelisted_programs.push(Token2022::id()); diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index bc21e2d38d..fbf629a34d 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1393,7 +1393,7 @@ pub mod drift { } pub fn update_perp_market_contract_tier( - ctx: Context, + ctx: Context, contract_tier: ContractTier, ) -> Result<()> { handle_update_perp_market_contract_tier(ctx, contract_tier) @@ -1802,24 +1802,6 @@ pub mod drift { handle_initialize_high_leverage_mode_config(ctx, max_users) } - pub fn initialize_lp_pool( - ctx: Context, - lp_pool_id: u8, - min_mint_fee: i64, - max_aum: u128, - max_settle_quote_amount_per_market: u64, - whitelist_mint: Pubkey, - ) -> Result<()> { - handle_initialize_lp_pool( - ctx, - lp_pool_id, - min_mint_fee, - max_aum, - max_settle_quote_amount_per_market, - whitelist_mint, - ) - } - pub fn update_high_leverage_mode_config( ctx: Context, max_users: u32, @@ -1934,6 +1916,24 @@ pub mod drift { handle_change_approved_builder(ctx, builder, max_fee_bps, add) } + pub fn initialize_lp_pool( + ctx: Context, + lp_pool_id: u8, + min_mint_fee: i64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + whitelist_mint: Pubkey, + ) -> Result<()> { + handle_initialize_lp_pool( + ctx, + lp_pool_id, + min_mint_fee, + max_aum, + max_settle_quote_amount_per_market, + whitelist_mint, + ) + } + pub fn update_feature_bit_flags_settle_lp_pool( ctx: Context, enable: bool, diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs index 2e4bf00532..cb35bf743d 100644 --- a/programs/drift/src/math/lp_pool.rs +++ b/programs/drift/src/math/lp_pool.rs @@ -5,7 +5,8 @@ pub mod perp_lp_pool_settlement { use crate::error::ErrorCode; use crate::math::casting::Cast; use crate::math::constants::QUOTE_PRECISION_U64; - use crate::state::spot_market::SpotBalanceType; + use crate::math::spot_balance::get_token_amount; + use crate::state::spot_market::{SpotBalance, SpotBalanceType}; use crate::{ math::safe_math::SafeMath, state::{amm_cache::CacheInfo, perp_market::PerpMarket, spot_market::SpotMarket}, @@ -55,6 +56,8 @@ pub mod perp_lp_pool_settlement { pub fn validate_settlement_amount( ctx: &SettlementContext, result: &SettlementResult, + perp_market: &PerpMarket, + quote_spot_market: &SpotMarket, ) -> Result<()> { if result.amount_transferred > ctx.max_settle_quote_amount { msg!( @@ -64,6 +67,49 @@ pub mod perp_lp_pool_settlement { ); return Err(ErrorCode::LpPoolSettleInvariantBreached.into()); } + + if result.direction == SettlementDirection::ToLpPool { + if result.fee_pool_used > 0 { + let fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.balance(), + quote_spot_market, + &SpotBalanceType::Deposit, + )?; + validate!( + fee_pool_token_amount >= result.fee_pool_used, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Fee pool balance insufficient for settlement: {} < {}", + fee_pool_token_amount, + result.fee_pool_used + )?; + } + + if result.pnl_pool_used > 0 { + let pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.balance(), + quote_spot_market, + &SpotBalanceType::Deposit, + )?; + validate!( + pnl_pool_token_amount >= result.pnl_pool_used, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Pnl pool balance insufficient for settlement: {} < {}", + pnl_pool_token_amount, + result.pnl_pool_used + )?; + } + } + if result.direction == SettlementDirection::FromLpPool { + validate!( + ctx.quote_constituent_token_balance + .saturating_sub(result.amount_transferred) + >= QUOTE_PRECISION_U64, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Quote constituent token balance insufficient for settlement: {} < {}", + ctx.quote_constituent_token_balance, + result.amount_transferred + )?; + } Ok(()) } diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index ed73a4801b..090213235c 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -285,16 +285,6 @@ impl<'a> AccountZeroCopyMut<'a, CacheInfo, AmmCacheFixed> { perp_market: &PerpMarket, quote_market: &SpotMarket, ) -> DriftResult<()> { - if perp_market.lp_fee_transfer_scalar == 0 - && perp_market.lp_exchange_fee_excluscion_scalar == 0 - { - msg!( - "lp_fee_transfer_scalar and lp_net_pnl_transfer_scalar are 0 for perp market {}. not updating quote amount owed in cache", - perp_market.market_index - ); - return Ok(()); - } - let cached_info = self.get_mut(perp_market.market_index as u32); let fee_pool_token_amount = get_token_amount( diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 4acab91a58..86c6556b00 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -60,7 +60,7 @@ pub const MAX_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10u64; mod tests; #[account(zero_copy(unsafe))] -#[derive(Default, Debug)] +#[derive(Debug)] #[repr(C)] pub struct LPPool { /// address of the vault. @@ -127,11 +127,45 @@ pub struct LPPool { pub lp_pool_id: u8, - pub padding: [u8; 14], + pub padding: [u8; 174], +} + +impl Default for LPPool { + fn default() -> Self { + Self { + pubkey: Pubkey::default(), + mint: Pubkey::default(), + whitelist_mint: Pubkey::default(), + constituent_target_base: Pubkey::default(), + constituent_correlations: Pubkey::default(), + max_aum: 0, + last_aum: 0, + cumulative_quote_sent_to_perp_markets: 0, + cumulative_quote_received_from_perp_markets: 0, + total_mint_redeem_fees_paid: 0, + last_aum_slot: 0, + max_settle_quote_amount: 0, + last_hedge_ts: 0, + mint_redeem_id: 0, + settle_id: 0, + min_mint_fee: 0, + token_supply: 0, + volatility: 0, + constituents: 0, + quote_consituent_index: 0, + bump: 0, + gamma_execution: 0, + xi: 0, + target_oracle_delay_fee_bps_per_10_slots: 0, + target_position_delay_fee_bps_per_10_slots: 0, + lp_pool_id: 0, + padding: [0u8; 174], + } + } } impl Size for LPPool { - const SIZE: usize = 344; + const SIZE: usize = 504; } impl LPPool { @@ -218,14 +252,14 @@ impl LPPool { correlation, )?; - in_fee += self.get_target_uncertainty_fees( + in_fee = in_fee.safe_add(self.get_target_uncertainty_fees( in_target_position_slot_delay, in_target_oracle_slot_delay, - )?; - out_fee += self.get_target_uncertainty_fees( + )?)?; + out_fee = out_fee.safe_add(self.get_target_uncertainty_fees( out_target_position_slot_delay, out_target_oracle_slot_delay, - )?; + )?)?; in_fee = in_fee.min(MAX_SWAP_FEE); out_fee = out_fee.min(MAX_SWAP_FEE); @@ -655,7 +689,7 @@ impl LPPool { return Ok(0); } let elapsed = delay.saturating_sub(threshold); - let blocks = (elapsed + 9) / 10; + let blocks = elapsed.safe_add(9)?.safe_div(10)?; let fee_bps = (blocks as u128).safe_mul(per_10_slot_bps as u128)?; let fee = fee_bps .safe_mul(PERCENTAGE_PRECISION)? @@ -871,7 +905,7 @@ impl ConstituentSpotBalance { } #[account(zero_copy(unsafe))] -#[derive(Default, Debug, BorshDeserialize, BorshSerialize)] +#[derive(Debug)] #[repr(C)] pub struct Constituent { /// address of the constituent @@ -945,11 +979,51 @@ pub struct Constituent { // Status pub status: u8, pub paused_operations: u8, - pub _padding: [u8; 2], + pub _padding: [u8; 162], +} + +impl Default for Constituent { + fn default() -> Self { + Self { + pubkey: Pubkey::default(), + mint: Pubkey::default(), + lp_pool: Pubkey::default(), + vault: Pubkey::default(), + total_swap_fees: 0, + spot_balance: ConstituentSpotBalance::default(), + last_spot_balance_token_amount: 0, + cumulative_spot_interest_accrued_token_amount: 0, + max_weight_deviation: 0, + swap_fee_min: 0, + swap_fee_max: 0, + max_borrow_token_amount: 0, + vault_token_balance: 0, + last_oracle_price: 0, + last_oracle_slot: 0, + oracle_staleness_threshold: 0, + flash_loan_initial_token_amount: 0, + next_swap_id: 0, + derivative_weight: 0, + volatility: 0, + constituent_derivative_depeg_threshold: 0, + constituent_derivative_index: -1, + spot_market_index: 0, + constituent_index: 0, + decimals: 0, + bump: 0, + vault_bump: 0, + gamma_inventory: 0, + gamma_execution: 0, + xi: 0, + status: 0, + paused_operations: 0, + _padding: [0; 162], + } + } } impl Size for Constituent { - const SIZE: usize = 320; + const SIZE: usize = 480; } #[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 5dfe3d9147..5a6b441c1c 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -762,7 +762,7 @@ impl PerpMarket { let last_fill_price = self.last_fill_price; - let mark_price_5min_twap = self.amm.last_mark_price_twap; + let mark_price_5min_twap = self.amm.last_mark_price_twap_5min; let last_oracle_price_twap_5min = self.amm.historical_oracle_data.last_oracle_price_twap_5min; diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index e960c71d91..3f09e5c58c 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -295,7 +295,7 @@ mod get_trigger_price { .get_trigger_price(oracle_price, now, true) .unwrap(); - assert_eq!(trigger_price, 109147085925); + assert_eq!(trigger_price, 109144736794); } #[test] diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index d3e8b39b7b..d02e743fea 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1066,6 +1066,40 @@ export class AdminClient extends DriftClient { ); } + public async updatePerpMarketLpPoolId( + perpMarketIndex: number, + lpPoolId: number + ) { + const updatePerpMarketLpPoolIIx = await this.getUpdatePerpMarketLpPoolIdIx( + perpMarketIndex, + lpPoolId + ); + + const tx = await this.buildTransaction(updatePerpMarketLpPoolIIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketLpPoolIdIx( + perpMarketIndex: number, + lpPoolId: number + ): Promise { + return await this.program.instruction.updatePerpMarketLpPoolId(lpPoolId, { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + }, + }); + } + public async updatePerpMarketLpPoolStatus( perpMarketIndex: number, lpStatus: number diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts index 4ac41a4357..ff8782c28c 100644 --- a/sdk/src/constituentMap/constituentMap.ts +++ b/sdk/src/constituentMap/constituentMap.ts @@ -15,7 +15,7 @@ import { ZSTDDecoder } from 'zstddec'; import { encodeName } from '../userName'; import { getLpPoolPublicKey } from '../addresses/pda'; -const MAX_CONSTITUENT_SIZE_BYTES = 320; // TODO: update this when account is finalized +const MAX_CONSTITUENT_SIZE_BYTES = 480; // TODO: update this when account is finalized export type ConstituentMapConfig = { driftClient: DriftClient; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index e402e22c48..9177d3a185 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -5929,11 +5929,6 @@ "name": "perpMarket", "isMut": true, "isSigner": false - }, - { - "name": "ammCache", - "isMut": true, - "isSigner": false } ], "args": [ @@ -7315,88 +7310,6 @@ } ] }, - { - "name": "initializeLpPool", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "lpPool", - "isMut": true, - "isSigner": false - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "lpPoolTokenVault", - "isMut": true, - "isSigner": false - }, - { - "name": "ammConstituentMapping", - "isMut": true, - "isSigner": false - }, - { - "name": "constituentTargetBase", - "isMut": true, - "isSigner": false - }, - { - "name": "constituentCorrelations", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "lpPoolId", - "type": "u8" - }, - { - "name": "minMintFee", - "type": "i64" - }, - { - "name": "maxAum", - "type": "u128" - }, - { - "name": "maxSettleQuoteAmountPerMarket", - "type": "u64" - }, - { - "name": "whitelistMint", - "type": "publicKey" - } - ] - }, { "name": "updateHighLeverageModeConfig", "accounts": [ @@ -7847,6 +7760,88 @@ } ] }, + { + "name": "initializeLpPool", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolId", + "type": "u8" + }, + { + "name": "minMintFee", + "type": "i64" + }, + { + "name": "maxAum", + "type": "u128" + }, + { + "name": "maxSettleQuoteAmountPerMarket", + "type": "u64" + }, + { + "name": "whitelistMint", + "type": "publicKey" + } + ] + }, { "name": "updateFeatureBitFlagsSettleLpPool", "accounts": [ @@ -9919,7 +9914,7 @@ "type": { "array": [ "u8", - 14 + 174 ] } } @@ -10109,7 +10104,7 @@ "type": { "array": [ "u8", - 2 + 162 ] } } @@ -19751,6 +19746,11 @@ "code": 6342, "name": "Unauthorized", "msg": "Unauthorized for operation" + }, + { + "code": 6343, + "name": "InvalidLpPoolId", + "msg": "Invalid Lp Pool Id for Operation" } ] } \ No newline at end of file diff --git a/tests/lpPool.ts b/tests/lpPool.ts index fffd5c9e58..610692c584 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -1210,6 +1210,7 @@ describe('LP Pool', () => { getAmmCachePublicKey(program.programId) )) as AmmCache; // No more usdc left in the constituent vault + console.log('constituentVault.amount', constituentVault.amount.toString()); assert(constituent.vaultTokenBalance.eq(QUOTE_PRECISION)); assert(new BN(constituentVault.amount.toString()).eq(QUOTE_PRECISION)); diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index 544cee85b2..1641f9ed98 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -416,6 +416,7 @@ describe('LP Pool', () => { new BN(200 * PEG_PRECISION.toNumber()) ); await adminClient.updatePerpMarketLpPoolStatus(i, 1); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(i, 100); await sleep(50); } diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index ae28ac8594..b6245e9f2e 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -57,10 +57,17 @@ import dotenv from 'dotenv'; import { DexInstructions, Market, OpenOrders } from '@project-serum/serum'; import { listMarket, SERUM, makePlaceOrderTransaction } from './serumHelper'; import { NATIVE_MINT } from '@solana/spl-token'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; dotenv.config(); describe('LP Pool', () => { const program = anchor.workspace.Drift as Program; + // Align account (de)serialization with on-chain zero-copy layouts + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); let bankrunContextWrapper: BankrunContextWrapper; let bulkAccountLoader: TestBulkAccountLoader; @@ -149,6 +156,8 @@ describe('LP Pool', () => { type: 'polling', accountLoader: bulkAccountLoader, }, + // Ensure the client uses the same custom coder + coder: new CustomBorshCoder(program.idl), }); await adminClient.initialize(usdcMint.publicKey, true); await adminClient.subscribe(); From 4b5735a7107343c2723eafd53ef24a002e04e7b5 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:04:58 -0700 Subject: [PATCH 147/159] add additional settle pnl invariant check --- programs/drift/src/instructions/keeper.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index f0a09b992d..c0c465395a 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -3334,6 +3334,10 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( let lp_pool_key = ctx.accounts.lp_pool.key(); let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + let tvl_before = quote_market + .get_tvl()? + .safe_add(quote_constituent.vault_token_balance as u128)?; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, @@ -3547,6 +3551,18 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( ctx.accounts.quote_token_vault.amount, )?; + let tvl_after = quote_market + .get_tvl()? + .safe_add(quote_constituent.vault_token_balance as u128)?; + + validate!( + tvl_before.safe_sub(tvl_after)? <= 10, + ErrorCode::LpPoolSettleInvariantBreached, + "LP pool settlement would decrease TVL: {} -> {}", + tvl_before, + tvl_after + )?; + Ok(()) } From 71dd9c101b1eb70ad6c6b2f7f234bfe9ce5d2254 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:00:06 -0700 Subject: [PATCH 148/159] create separate hot wallet for lp pool --- programs/drift/src/ids.rs | 5 +++ programs/drift/src/instructions/lp_admin.rs | 34 ++++++++++----------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index f7ac19adda..a37dd452fb 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -123,6 +123,11 @@ pub mod titan_mainnet_argos_v1 { declare_id!("T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"); } +pub mod lp_pool_hot_wallet { + use solana_program::declare_id; + declare_id!("GP9qHLX8rx4BgRULGPV1poWQPdGuzbxGbvTB12DfmwFk"); +} + pub const WHITELISTED_SWAP_PROGRAMS: &[solana_program::pubkey::Pubkey] = &[ serum_program::id(), jupiter_mainnet_3::id(), diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index ccd117a72b..d2e3064ab2 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1,6 +1,6 @@ use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; use crate::error::ErrorCode; -use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet, WHITELISTED_SWAP_PROGRAMS}; +use crate::ids::{lp_pool_hot_wallet, lp_pool_swap_wallet, WHITELISTED_SWAP_PROGRAMS}; use crate::instructions::optional_accounts::{get_token_mint, load_maps, AccountMaps}; use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; use crate::math::safe_math::SafeMath; @@ -597,7 +597,7 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( { let state = &ctx.accounts.state; validate!( - admin.key() == admin_hot_wallet::id() || admin.key() == state.admin, + admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin, ErrorCode::Unauthorized, "Wrong signer for lp taker swap" )?; @@ -1001,7 +1001,10 @@ pub fn handle_override_amm_cache_info<'c: 'info, 'info>( id: u8, )] pub struct InitializeLpPool<'info> { - #[account(mut)] + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] pub admin: Signer<'info>, #[account( init, @@ -1050,9 +1053,6 @@ pub struct InitializeLpPool<'info> { )] pub constituent_correlations: Box>, - #[account( - has_one = admin - )] pub state: Box>, pub token_program: Program<'info, Token>, @@ -1068,7 +1068,7 @@ pub struct InitializeConstituent<'info> { pub state: Box>, #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, @@ -1138,7 +1138,7 @@ pub struct UpdateConstituentParams<'info> { pub constituent_target_base: Box>, #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub state: Box>, @@ -1162,7 +1162,7 @@ pub struct UpdateConstituentStatus<'info> { pub struct UpdateConstituentPausedOperations<'info> { #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub state: Box>, @@ -1176,7 +1176,7 @@ pub struct UpdateLpPoolParams<'info> { pub lp_pool: AccountLoader<'info, LPPool>, #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub state: Box>, @@ -1196,7 +1196,7 @@ pub struct AddAmmConstituentMappingDatum { pub struct AddAmmConstituentMappingData<'info> { #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub lp_pool: AccountLoader<'info, LPPool>, @@ -1230,7 +1230,7 @@ pub struct AddAmmConstituentMappingData<'info> { pub struct UpdateAmmConstituentMappingData<'info> { #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub lp_pool: AccountLoader<'info, LPPool>, @@ -1249,7 +1249,7 @@ pub struct UpdateAmmConstituentMappingData<'info> { pub struct RemoveAmmConstituentMappingData<'info> { #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub lp_pool: AccountLoader<'info, LPPool>, @@ -1271,7 +1271,7 @@ pub struct RemoveAmmConstituentMappingData<'info> { pub struct UpdateConstituentCorrelation<'info> { #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub lp_pool: AccountLoader<'info, LPPool>, @@ -1294,7 +1294,7 @@ pub struct LPTakerSwap<'info> { pub state: Box>, #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == lp_pool_swap_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == lp_pool_swap_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, /// Signer token accounts @@ -1363,7 +1363,7 @@ pub struct UpdatePerpMarketLpPoolStatus<'info> { pub struct UpdateInitialAmmCacheInfo<'info> { #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub state: Box>, pub admin: Signer<'info>, @@ -1379,7 +1379,7 @@ pub struct UpdateInitialAmmCacheInfo<'info> { pub struct ResetAmmCache<'info> { #[account( mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, pub state: Box>, From 3c6ad685b8eef3ca02dd3d80369b2d1f3426ebd3 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:14:43 -0700 Subject: [PATCH 149/159] remove any unused fields --- programs/drift/src/instructions/lp_admin.rs | 2 +- programs/drift/src/state/lp_pool.rs | 12 ++--------- programs/drift/src/state/lp_pool/tests.rs | 4 ++-- sdk/src/idl/drift.json | 22 +-------------------- sdk/src/types.ts | 1 - 5 files changed, 6 insertions(+), 35 deletions(-) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index d2e3064ab2..8ce075b134 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -64,7 +64,7 @@ pub fn handle_initialize_lp_pool( last_aum: 0, last_aum_slot: 0, max_settle_quote_amount: max_settle_quote_amount_per_market, - last_hedge_ts: 0, + _padding: 0, total_mint_redeem_fees_paid: 0, bump: ctx.bumps.lp_pool, min_mint_fee, diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 86c6556b00..1f7c2b5849 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -97,7 +97,7 @@ pub struct LPPool { pub max_settle_quote_amount: u64, /// timestamp of last vAMM revenue rebalance - pub last_hedge_ts: u64, + pub _padding: u64, /// Every mint/redeem has a monotonically increasing id. This is the next id to use pub mint_redeem_id: u64, @@ -145,7 +145,7 @@ impl Default for LPPool { total_mint_redeem_fees_paid: 0, last_aum_slot: 0, max_settle_quote_amount: 0, - last_hedge_ts: 0, + _padding: 0, mint_redeem_id: 0, settle_id: 0, min_mint_fee: 0, @@ -1313,14 +1313,6 @@ impl Default for ConstituentTargetBase { } } -#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] -pub enum WeightValidationFlags { - NONE = 0b0000_0000, - EnforceTotalWeight100 = 0b0000_0001, - NoNegativeWeights = 0b0000_0010, - NoOverweight = 0b0000_0100, -} - impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { pub fn get_target_weight( &self, diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index 24cab5e03d..ef9153b723 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -782,7 +782,7 @@ mod swap_tests { ) { let lp_pool = LPPool { last_aum, - last_hedge_ts: 0, + _padding: 0, min_mint_fee: 0, ..LPPool::default() }; @@ -974,7 +974,7 @@ mod swap_tests { ) { let lp_pool = LPPool { last_aum, - last_hedge_ts: 0, + _padding: 0, min_mint_fee: 100, // 1 bps ..LPPool::default() }; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index ebb729d75e..1d61c52f13 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -9845,7 +9845,7 @@ "type": "u64" }, { - "name": "lastHedgeTs", + "name": "padding", "docs": [ "timestamp of last vAMM revenue rebalance" ], @@ -15445,26 +15445,6 @@ ] } }, - { - "name": "WeightValidationFlags", - "type": { - "kind": "enum", - "variants": [ - { - "name": "NONE" - }, - { - "name": "EnforceTotalWeight100" - }, - { - "name": "NoNegativeWeights" - }, - { - "name": "NoOverweight" - } - ] - } - }, { "name": "MarginCalculationMode", "type": { diff --git a/sdk/src/types.ts b/sdk/src/types.ts index befe097445..e2d7302d23 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1785,7 +1785,6 @@ export type LPPoolAccount = { totalMintRedeemFeesPaid: BN; lastAumSlot: BN; maxSettleQuoteAmount: BN; - lastHedgeTs: BN; mintRedeemId: BN; settleId: BN; minMintFee: BN; From af376f2483860a1d87f986927585ad8cc983bc0b Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:19:29 -0700 Subject: [PATCH 150/159] add oracle map logging argument --- programs/drift/src/instructions/lp_pool.rs | 8 ++++++++ programs/drift/src/state/lp_pool.rs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 9f75a6d2f1..cfb4856751 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -336,6 +336,7 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -349,6 +350,7 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { @@ -559,6 +561,7 @@ pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -572,6 +575,7 @@ pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); @@ -699,6 +703,7 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -918,6 +923,7 @@ pub fn handle_view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( in_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let in_oracle = in_oracle.clone(); @@ -1065,6 +1071,7 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let out_oracle = *out_oracle; @@ -1320,6 +1327,7 @@ pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( out_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let out_oracle = out_oracle.clone(); diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 1f7c2b5849..a00970599c 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -771,6 +771,7 @@ impl LPPool { spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action( oracle_and_validity.1, @@ -1774,6 +1775,7 @@ pub fn update_constituent_target_base_for_derivatives( parent_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action( parent_oracle_price_and_validity.1, @@ -1811,6 +1813,7 @@ pub fn update_constituent_target_base_for_derivatives( constituent_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action( constituent_oracle_price_and_validity.1, From 503fa41d332c82f64ce94c1497a9c9fcd0a5dff6 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:33:38 -0700 Subject: [PATCH 151/159] generalize lp pool test failure --- tests/lpPool.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 610692c584..27ebbabad5 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -1727,9 +1727,10 @@ describe('LP Pool', () => { await adminClient.getUpdateLpPoolAumIxs(oldLpPool, [0, 1, 2]) ); await adminClient.sendTransaction(settleTx); + assert.fail('Should have thrown'); } catch (e) { console.log(e.message); - assert(e.message.includes('0x18c7')); + assert(e.message.includes('0x18')); } }); }); From e6dd77acf513e160b7660c6727bd084e814d305c Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:40:16 -0700 Subject: [PATCH 152/159] unified swap mode compatibility --- sdk/src/adminClient.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index d02e743fea..da4d9cc8a5 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -84,11 +84,8 @@ import { PROGRAM_ID as PHOENIX_PROGRAM_ID } from '@ellipsis-labs/phoenix-sdk'; import { DRIFT_ORACLE_RECEIVER_ID } from './config'; import { getFeedIdUint8Array } from './util/pythOracleUtils'; import { FUEL_RESET_LOG_ACCOUNT } from './constants/txConstants'; -import { - JupiterClient, - QuoteResponse, - SwapMode, -} from './jupiter/jupiterClient'; +import { JupiterClient, QuoteResponse } from './jupiter/jupiterClient'; +import { SwapMode } from './swap/UnifiedSwapClient'; const OPENBOOK_PROGRAM_ID = new PublicKey( 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb' From d14dc148db5d9018572cecf0fc05633e4c176639 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:02:55 -0700 Subject: [PATCH 153/159] clippy and whitelist changes --- programs/drift/src/instructions/admin.rs | 2 +- programs/drift/src/instructions/lp_admin.rs | 14 +++++--------- programs/drift/src/state/lp_pool.rs | 2 +- tests/lpPool.ts | 2 -- tests/lpPoolCUs.ts | 1 - tests/lpPoolSwap.ts | 7 ++----- 6 files changed, 9 insertions(+), 19 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index f806412f02..07b0913f48 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -5488,7 +5488,7 @@ pub struct InitializePerpMarket<'info> { mut, seeds = [AMM_POSITIONS_CACHE.as_ref()], bump = amm_cache.bump, - realloc = AmmCache::space(amm_cache.cache.len() + 1 as usize), + realloc = AmmCache::space(amm_cache.cache.len() + 1_usize), realloc::payer = admin, realloc::zero = false, )] diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 8ce075b134..28e6026a44 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -19,6 +19,7 @@ use crate::validate; use crate::{controller, load_mut}; use anchor_lang::prelude::*; use anchor_lang::Discriminator; +use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::Token; use anchor_spl::token_2022::Token2022; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; @@ -639,12 +640,6 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( // Make sure we have enough balance to do the swap let constituent_in_token_account = &ctx.accounts.constituent_in_token_account; - - msg!("amount_in: {}", amount_in); - msg!( - "constituent_in_token_account.amount: {}", - constituent_in_token_account.amount - ); validate!( amount_in <= constituent_in_token_account.amount, ErrorCode::InvalidSwap, @@ -761,6 +756,7 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( } } else { let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); + whitelisted_programs.push(AssociatedToken::id()); whitelisted_programs.push(Token::id()); whitelisted_programs.push(Token2022::id()); whitelisted_programs.push(marinade_mainnet::ID); @@ -1079,7 +1075,7 @@ pub struct InitializeConstituent<'info> { mut, seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], bump = constituent_target_base.bump, - realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1 as usize), + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1_usize), realloc::payer = admin, realloc::zero = false, )] @@ -1089,7 +1085,7 @@ pub struct InitializeConstituent<'info> { mut, seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], bump = constituent_correlations.bump, - realloc = ConstituentCorrelations::space(constituent_target_base.targets.len() + 1 as usize), + realloc = ConstituentCorrelations::space(constituent_target_base.targets.len() + 1_usize), realloc::payer = admin, realloc::zero = false, )] @@ -1214,7 +1210,7 @@ pub struct AddAmmConstituentMappingData<'info> { mut, seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], bump, - realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1 as usize), + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1_usize), realloc::payer = admin, realloc::zero = false, )] diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index a00970599c..106945fa5a 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1454,7 +1454,7 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { ); cell.target_base = target_base.cast::()?; - if slot.saturating_sub(oldest_position_slot) <= MAX_STALENESS_FOR_TARGET_CALC { + if slot.saturating_sub(oldest_position_slot) == MAX_STALENESS_FOR_TARGET_CALC { cell.last_position_slot = slot; } else { msg!( diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 27ebbabad5..91470291ed 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -28,7 +28,6 @@ import { QUOTE_PRECISION, getLpPoolPublicKey, getAmmConstituentMappingPublicKey, - encodeName, getConstituentTargetBasePublicKey, PERCENTAGE_PRECISION, PRICE_PRECISION, @@ -1688,7 +1687,6 @@ describe('LP Pool', () => { it('can initialize multiple lp pools', async () => { const newLpPoolId = 1; - const newLpPoolKey = getLpPoolPublicKey(program.programId, newLpPoolId); await adminClient.initializeLpPool( newLpPoolId, diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index 1641f9ed98..d256c56ed3 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -28,7 +28,6 @@ import { QUOTE_PRECISION, getLpPoolPublicKey, getAmmConstituentMappingPublicKey, - encodeName, getConstituentTargetBasePublicKey, PERCENTAGE_PRECISION, PRICE_PRECISION, diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index b6245e9f2e..cbd699ec9f 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -13,7 +13,6 @@ import { TestClient, QUOTE_PRECISION, getLpPoolPublicKey, - encodeName, getConstituentTargetBasePublicKey, PERCENTAGE_PRECISION, PRICE_PRECISION, @@ -503,13 +502,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})` ); }); From d29166bdd2e0a0ad9d6181473091437ae955fc9c Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:19:27 -0400 Subject: [PATCH 154/159] cargo tests pass --- programs/drift/src/error.rs | 2 + programs/drift/src/instructions/admin.rs | 36 +++-------- programs/drift/src/instructions/keeper.rs | 4 +- programs/drift/src/instructions/lp_admin.rs | 14 ----- programs/drift/src/instructions/lp_pool.rs | 43 +++++++------ programs/drift/src/lib.rs | 12 +--- programs/drift/src/state/amm_cache.rs | 45 ++++++++++---- programs/drift/src/state/lp_pool.rs | 30 +++++---- programs/drift/src/state/lp_pool/tests.rs | 69 +++++++++++++++------ sdk/src/adminClient.ts | 14 +++-- sdk/src/types.ts | 1 + 11 files changed, 152 insertions(+), 118 deletions(-) diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index ab38f477bc..4c5bbcfa91 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -692,6 +692,8 @@ pub enum ErrorCode { Unauthorized, #[msg("Invalid Lp Pool Id for Operation")] InvalidLpPoolId, + #[msg("MarketIndexNotFoundAmmCache")] + MarketIndexNotFoundAmmCache, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 456886ea29..ada545f781 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1107,17 +1107,6 @@ pub fn handle_initialize_perp_market( safe_increment!(state.number_of_markets, 1); - let amm_cache = &mut ctx.accounts.amm_cache; - let current_len = amm_cache.cache.len(); - amm_cache - .cache - .resize_with(current_len + 1, CacheInfo::default); - let current_market_info = amm_cache.cache.get_mut(current_len).unwrap(); - current_market_info.slot = clock_slot; - current_market_info.oracle = perp_market.amm.oracle; - current_market_info.oracle_source = u8::from(perp_market.amm.oracle_source); - amm_cache.validate(state)?; - controller::amm::update_concentration_coef(perp_market, concentration_coef_scale)?; crate::dlog!(oracle_price); @@ -1140,11 +1129,11 @@ pub fn handle_initialize_amm_cache(ctx: Context) -> Result<( Ok(()) } -pub fn handle_resize_amm_cache(ctx: Context) -> Result<()> { +pub fn handle_add_market_to_amm_cache(ctx: Context) -> Result<()> { let amm_cache = &mut ctx.accounts.amm_cache; - let state = &ctx.accounts.state; + let perp_market = ctx.accounts.perp_market.load()?; let current_size = amm_cache.cache.len(); - let new_size = (state.number_of_markets as usize).min(current_size + 20_usize); + let new_size = current_size.saturating_add(1); msg!( "resizing amm cache from {} entries to {}", @@ -1152,16 +1141,10 @@ pub fn handle_resize_amm_cache(ctx: Context) -> Result<()> { new_size ); - let growth = new_size.saturating_sub(current_size); - validate!( - growth <= 20, - ErrorCode::DefaultError, - "cannot grow amm_cache by more than 20 entries in a single resize (requested +{})", - growth - )?; - - amm_cache.cache.resize_with(new_size, CacheInfo::default); - amm_cache.validate(state)?; + amm_cache.cache.resize_with(new_size, || CacheInfo { + market_index: perp_market.market_index, + ..CacheInfo::default() + }); Ok(()) } @@ -5547,7 +5530,7 @@ pub struct InitializeAmmCache<'info> { } #[derive(Accounts)] -pub struct ResizeAmmCache<'info> { +pub struct AddMarketToAmmCache<'info> { #[account( mut, constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin @@ -5558,11 +5541,12 @@ pub struct ResizeAmmCache<'info> { mut, seeds = [AMM_POSITIONS_CACHE.as_ref()], bump, - realloc = AmmCache::space(amm_cache.cache.len() + (state.number_of_markets as usize - amm_cache.cache.len()).min(20_usize)), + realloc = AmmCache::space(amm_cache.cache.len() + 1), realloc::payer = admin, realloc::zero = false, )] pub amm_cache: Box>, + pub perp_market: AccountLoader<'info, PerpMarket>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, } diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index c0c465395a..d4308a5313 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -3378,7 +3378,7 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( continue; } - let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + let cached_info = amm_cache.get_for_market_index_mut(perp_market.market_index)?; // Early validation checks if slot.saturating_sub(cached_info.oracle_slot) > SETTLE_AMM_ORACLE_MAX_DELAY { @@ -3594,7 +3594,7 @@ pub fn handle_update_amm_cache<'c: 'info, 'info>( if perp_market.lp_status == 0 { continue; } - let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + let cached_info = amm_cache.get_for_market_index_mut(perp_market.market_index)?; validate!( perp_market.oracle_id() == cached_info.oracle_id()?, diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index 28e6026a44..21630eae20 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -922,20 +922,6 @@ pub fn handle_update_initial_amm_cache_info<'c: 'info, 'info>( Ok(()) } -pub fn handle_reset_amm_cache(ctx: Context) -> Result<()> { - let state = &ctx.accounts.state; - let amm_cache = &mut ctx.accounts.amm_cache; - - amm_cache.cache.clear(); - amm_cache - .cache - .resize_with(state.number_of_markets as usize, CacheInfo::default); - amm_cache.validate(state)?; - - msg!("AMM cache reset. markets: {}", state.number_of_markets); - Ok(()) -} - #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] pub struct OverrideAmmCacheParams { pub quote_owed_from_lp_pool: Option, diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index cfb4856751..3108af7ca9 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -43,6 +43,7 @@ use crate::{ }, validate, }; +use std::collections::BTreeMap; use std::iter::Peekable; use std::slice::Iter; @@ -96,31 +97,33 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( let constituent_map = ConstituentMap::load(&ConstituentSet::new(), &lp_pool_key, remaining_accounts)?; - let mut amm_inventories: Vec = - Vec::with_capacity(amm_cache.len() as usize); + let mut amm_inventories: BTreeMap = BTreeMap::new(); for (_, cache_info) in amm_cache.iter().enumerate() { if cache_info.lp_status_for_perp_market == 0 { continue; } - amm_inventories.push(AmmInventoryAndPricesAndSlots { - inventory: { - let scaled_position = cache_info - .position - .safe_mul(cache_info.amm_position_scalar as i64)? - .safe_div(100)?; - - scaled_position.clamp( - -cache_info.amm_inventory_limit, - cache_info.amm_inventory_limit, - ) + amm_inventories.insert( + cache_info.market_index, + AmmInventoryAndPricesAndSlots { + inventory: { + let scaled_position = cache_info + .position + .safe_mul(cache_info.amm_position_scalar as i64)? + .safe_div(100)?; + + scaled_position.clamp( + -cache_info.amm_inventory_limit, + cache_info.amm_inventory_limit, + ) + }, + price: cache_info.oracle_price, + last_oracle_slot: cache_info.oracle_slot, + last_position_slot: cache_info.slot, }, - price: cache_info.oracle_price, - last_oracle_slot: cache_info.oracle_slot, - last_position_slot: cache_info.slot, - }); + ); } - msg!("amm inventories: {:?}", amm_inventories); + msg!("amm inventories:{:?}", amm_inventories); if amm_inventories.is_empty() { msg!("No valid inventories found for constituent target weights update"); @@ -140,7 +143,7 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( constituent_target_base.update_target_base( &amm_constituent_mapping, - amm_inventories.as_slice(), + &amm_inventories, constituent_indexes_and_decimals_and_prices.as_mut_slice(), slot, )?; @@ -1010,7 +1013,7 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( if cache_info.last_fee_pool_token_amount != 0 && cache_info.last_settle_slot != slot { msg!( "Market {} has not been settled in current slot. Last slot: {}", - i, + cache_info.market_index, cache_info.last_settle_slot ); return Err(ErrorCode::AMMCacheStale.into()); diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 3d13094ebf..7ec90f959e 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1020,10 +1020,10 @@ pub mod drift { handle_initialize_amm_cache(ctx) } - pub fn resize_amm_cache<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, ResizeAmmCache<'info>>, + pub fn add_market_to_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, AddMarketToAmmCache<'info>>, ) -> Result<()> { - handle_resize_amm_cache(ctx) + handle_add_market_to_amm_cache(ctx) } pub fn update_initial_amm_cache_info<'c: 'info, 'info>( @@ -2086,12 +2086,6 @@ pub mod drift { handle_override_amm_cache_info(ctx, market_index, override_params) } - pub fn reset_amm_cache<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, ResetAmmCache<'info>>, - ) -> Result<()> { - handle_reset_amm_cache(ctx) - } - pub fn lp_pool_swap<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, in_market_index: u16, diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index 59982ce15c..957d7d392f 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -53,11 +53,12 @@ pub struct CacheInfo { pub amm_inventory_limit: i64, pub oracle_price: i64, pub oracle_slot: u64, + pub market_index: u16, pub oracle_source: u8, pub oracle_validity: u8, pub lp_status_for_perp_market: u8, pub amm_position_scalar: u8, - pub _padding: [u8; 36], + pub _padding: [u8; 34], } impl Size for CacheInfo { @@ -86,7 +87,8 @@ impl Default for CacheInfo { quote_owed_from_lp_pool: 0i64, lp_status_for_perp_market: 0u8, amm_position_scalar: 0u8, - _padding: [0u8; 36], + market_index: 0u16, + _padding: [0u8; 34], } } } @@ -183,15 +185,6 @@ impl AmmCache { 8 + 8 + 4 + num_markets * CacheInfo::SIZE } - pub fn validate(&self, state: &State) -> DriftResult<()> { - validate!( - self.cache.len() <= state.number_of_markets as usize, - ErrorCode::DefaultError, - "Number of amm positions is no larger than number of markets" - )?; - Ok(()) - } - pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { let cache_info = self.cache.get_mut(perp_market.market_index as usize); if let Some(cache_info) = cache_info { @@ -238,6 +231,15 @@ impl AmmCache { impl_zero_copy_loader!(AmmCache, crate::id, AmmCacheFixed, CacheInfo); impl<'a> AccountZeroCopy<'a, CacheInfo, AmmCacheFixed> { + pub fn get_for_market_index(&self, market_index: u16) -> DriftResult<&CacheInfo> { + for cache_info in self.iter() { + if cache_info.market_index == market_index { + return Ok(cache_info); + } + } + Err(ErrorCode::MarketIndexNotFoundAmmCache.into()) + } + pub fn check_settle_staleness(&self, slot: u64, threshold_slot_diff: u64) -> DriftResult<()> { for (i, cache_info) in self.iter().enumerate() { if cache_info.slot == 0 { @@ -284,6 +286,27 @@ impl<'a> AccountZeroCopy<'a, CacheInfo, AmmCacheFixed> { } impl<'a> AccountZeroCopyMut<'a, CacheInfo, AmmCacheFixed> { + pub fn get_for_market_index_mut(&mut self, market_index: u16) -> DriftResult<&mut CacheInfo> { + let pos = { + let mut found: Option = None; + for i in 0..self.len() { + let cache_info = self.get(i); + if cache_info.market_index == market_index { + found = Some(i); + break; + } + } + found + }; + + if let Some(i) = pos { + Ok(self.get_mut(i)) + } else { + msg!("Market index not found in amm cache: {}", market_index); + Err(ErrorCode::MarketIndexNotFoundAmmCache.into()) + } + } + pub fn update_amount_owed_from_lp_pool( &mut self, perp_market: &PerpMarket, diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 106945fa5a..f975ce9e81 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1378,27 +1378,33 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { pub fn update_target_base( &mut self, mapping: &AccountZeroCopy<'a, AmmConstituentDatum, AmmConstituentMappingFixed>, - amm_inventory_and_prices: &[AmmInventoryAndPricesAndSlots], + amm_inventory_and_prices: &std::collections::BTreeMap, constituents_indexes_and_decimals_and_prices: &mut [ConstituentIndexAndDecimalAndPrice], slot: u64, ) -> DriftResult<()> { // Sorts by constituent index constituents_indexes_and_decimals_and_prices.sort_by_key(|c| c.constituent_index); - // Precompute notional by perp market index - let mut notionals_and_slots: Vec<(i128, u64, u64)> = - Vec::with_capacity(amm_inventory_and_prices.len()); - for &AmmInventoryAndPricesAndSlots { - inventory, - price, - last_oracle_slot, - last_position_slot, - } in amm_inventory_and_prices.iter() + let mut notionals_and_slots: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + for ( + &market_index, + &AmmInventoryAndPricesAndSlots { + inventory, + price, + last_oracle_slot, + last_position_slot, + .. + }, + ) in amm_inventory_and_prices.iter() { let notional = (inventory as i128) .safe_mul(price as i128)? .safe_div(BASE_PRECISION_I128)?; - notionals_and_slots.push((notional, last_oracle_slot, last_position_slot)); + notionals_and_slots.insert( + market_index, + (notional, last_oracle_slot, last_position_slot), + ); } let mut mapping_index = 0; @@ -1428,7 +1434,7 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { break; } if let Some((perp_notional, perp_last_oracle_slot, perp_last_position_slot)) = - notionals_and_slots.get(d.perp_market_index as usize) + notionals_and_slots.get(&d.perp_market_index) { target_notional = target_notional .saturating_add(perp_notional.saturating_mul(d.weight as i128)); diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index ef9153b723..3e76383af9 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -4,7 +4,7 @@ mod tests { BASE_PRECISION_I64, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64, QUOTE_PRECISION, }; use crate::state::lp_pool::*; - use std::{cell::RefCell, marker::PhantomData, vec}; + use std::{cell::RefCell, collections::BTreeMap, marker::PhantomData, vec}; fn amm_const_datum( perp_market_index: u16, @@ -68,32 +68,45 @@ mod tests { } }; - let amm_inventory_and_price: Vec = vec![ + let mut amm_inventory_and_price: BTreeMap = + BTreeMap::new(); + // key: perp market index + amm_inventory_and_price.insert( + 0, AmmInventoryAndPricesAndSlots { inventory: 4 * BASE_PRECISION_I64, price: 100_000 * PRICE_PRECISION_I64, last_oracle_slot: slot, last_position_slot: slot, - }, // $400k BTC + }, + ); // $400k BTC + amm_inventory_and_price.insert( + 1, AmmInventoryAndPricesAndSlots { inventory: 2000 * BASE_PRECISION_I64, price: 200 * PRICE_PRECISION_I64, last_oracle_slot: slot, last_position_slot: slot, - }, // $400k SOL + }, + ); // $400k SOL + amm_inventory_and_price.insert( + 2, AmmInventoryAndPricesAndSlots { inventory: 200 * BASE_PRECISION_I64, price: 1500 * PRICE_PRECISION_I64, last_oracle_slot: slot, last_position_slot: slot, - }, // $300k ETH + }, + ); // $300k ETH + amm_inventory_and_price.insert( + 3, AmmInventoryAndPricesAndSlots { inventory: 16500 * BASE_PRECISION_I64, price: PRICE_PRECISION_I64, last_oracle_slot: slot, last_position_slot: slot, - }, // $16.5k FARTCOIN - ]; + }, + ); // $16.5k FARTCOIN let mut constituents_indexes_and_decimals_and_prices = vec![ ConstituentIndexAndDecimalAndPrice { constituent_index: 0, @@ -145,7 +158,7 @@ mod tests { calculate_target_weight( datum.target_base.cast::().unwrap(), &SpotMarket::default_quote_market(), - amm_inventory_and_price.get(index).unwrap().price, + amm_inventory_and_price.get(&(index as u16)).unwrap().price, aum, ) .unwrap() @@ -189,13 +202,17 @@ mod tests { } }; - let amm_inventory_and_prices: Vec = - vec![AmmInventoryAndPricesAndSlots { + let mut amm_inventory_and_prices: BTreeMap = + BTreeMap::new(); + amm_inventory_and_prices.insert( + 0, + AmmInventoryAndPricesAndSlots { inventory: 1_000_000, price: 1_000_000, last_oracle_slot: slot, last_position_slot: slot, - }]; + }, + ); let mut constituents_indexes_and_decimals_and_prices = vec![ConstituentIndexAndDecimalAndPrice { constituent_index: 1, @@ -259,13 +276,17 @@ mod tests { }; let price = PRICE_PRECISION_I64; - let amm_inventory_and_prices: Vec = - vec![AmmInventoryAndPricesAndSlots { + let mut amm_inventory_and_prices: BTreeMap = + BTreeMap::new(); + amm_inventory_and_prices.insert( + 0, + AmmInventoryAndPricesAndSlots { inventory: BASE_PRECISION_I64, price, last_oracle_slot: slot, last_position_slot: slot, - }]; + }, + ); let mut constituents_indexes_and_decimals_and_prices = vec![ConstituentIndexAndDecimalAndPrice { constituent_index: 1, @@ -362,13 +383,17 @@ mod tests { } }; - let amm_inventory_and_prices: Vec = - vec![AmmInventoryAndPricesAndSlots { + let mut amm_inventory_and_prices: BTreeMap = + BTreeMap::new(); + amm_inventory_and_prices.insert( + 0, + AmmInventoryAndPricesAndSlots { inventory: 1_000_000_000, price: 1_000_000, last_oracle_slot: slot, last_position_slot: slot, - }]; + }, + ); let mut constituents_indexes_and_decimals_and_prices = vec![ ConstituentIndexAndDecimalAndPrice { constituent_index: 1, @@ -453,13 +478,17 @@ mod tests { } }; - let amm_inventory_and_prices: Vec = - vec![AmmInventoryAndPricesAndSlots { + let mut amm_inventory_and_prices: BTreeMap = + BTreeMap::new(); + amm_inventory_and_prices.insert( + 0, + AmmInventoryAndPricesAndSlots { inventory: 1_000_000, price: 142_000_000, last_oracle_slot: slot, last_position_slot: slot, - }]; + }, + ); let mut constituents_indexes_and_decimals_and_prices = vec![ConstituentIndexAndDecimalAndPrice { constituent_index: 1, diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 9698e13716..0974fa5f3f 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -674,10 +674,13 @@ export class AdminClient extends DriftClient { }); } - public async resizeAmmCache( + public async addMarketToAmmCache( + perpMarketIndex: number, txParams?: TxParams ): Promise { - const initializeAmmCacheIx = await this.getResizeAmmCacheIx(); + const initializeAmmCacheIx = await this.getAddMarketToAmmCacheIx( + perpMarketIndex + ); const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); @@ -686,13 +689,16 @@ export class AdminClient extends DriftClient { return txSig; } - public async getResizeAmmCacheIx(): Promise { - return await this.program.instruction.resizeAmmCache({ + public async getAddMarketToAmmCacheIx( + perpMarketIndex: number + ): Promise { + return await this.program.instruction.addMarketToAmmCache({ accounts: { state: await this.getStatePublicKey(), admin: this.useHotWalletAdmin ? this.wallet.publicKey : this.getStateAccount().admin, + perpMarket: this.getPerpMarketAccount(perpMarketIndex).pubkey, ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index bb15805693..ee5ff35422 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1891,6 +1891,7 @@ export type CacheInfo = { oracleValidity: number; lpStatusForPerpMarket: number; ammPositionScalar: number; + marketIndex: number; }; export type AmmCache = { From e2fe3abd483a333222c38bf41d7080677e5d8ddc Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Sun, 2 Nov 2025 19:07:29 -0500 Subject: [PATCH 155/159] refactor amm cache to only include collateralized markets --- programs/drift/src/instructions/admin.rs | 19 ++++----- programs/drift/src/state/amm_cache.rs | 2 - sdk/src/adminClient.ts | 19 ++------- sdk/src/idl/drift.json | 49 ++++++++---------------- tests/lpPool.ts | 6 ++- tests/lpPoolCUs.ts | 4 +- tests/lpPoolSwap.ts | 4 ++ 7 files changed, 41 insertions(+), 62 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index ada545f781..edd3cd69a4 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1132,6 +1132,16 @@ pub fn handle_initialize_amm_cache(ctx: Context) -> Result<( pub fn handle_add_market_to_amm_cache(ctx: Context) -> Result<()> { let amm_cache = &mut ctx.accounts.amm_cache; let perp_market = ctx.accounts.perp_market.load()?; + + for cache_info in amm_cache.cache.iter() { + validate!( + cache_info.market_index != perp_market.market_index, + ErrorCode::DefaultError, + "Market index {} already in amm cache", + perp_market.market_index + )?; + } + let current_size = amm_cache.cache.len(); let new_size = current_size.saturating_add(1); @@ -5494,15 +5504,6 @@ pub struct InitializePerpMarket<'info> { payer = admin )] pub perp_market: AccountLoader<'info, PerpMarket>, - #[account( - mut, - seeds = [AMM_POSITIONS_CACHE.as_ref()], - bump = amm_cache.bump, - realloc = AmmCache::space(amm_cache.cache.len() + 1_usize), - realloc::payer = admin, - realloc::zero = false, - )] - pub amm_cache: Box>, /// CHECK: checked in `initialize_perp_market` pub oracle: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index 957d7d392f..1e485b2289 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -10,11 +10,9 @@ use crate::state::oracle::MMOraclePriceData; use crate::state::oracle_map::OracleIdentifier; use crate::state::perp_market::PerpMarket; use crate::state::spot_market::{SpotBalance, SpotMarket}; -use crate::state::state::State; use crate::state::traits::Size; use crate::state::zero_copy::HasLen; use crate::state::zero_copy::{AccountZeroCopy, AccountZeroCopyMut}; -use crate::validate; use crate::OracleSource; use crate::{impl_zero_copy_loader, OracleGuardRails}; diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 0974fa5f3f..bb2fb35ae5 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -507,12 +507,6 @@ export class AdminClient extends DriftClient { ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; - const ammCachePublicKey = getAmmCachePublicKey(this.program.programId); - const ammCacheAccount = await this.connection.getAccountInfo( - ammCachePublicKey - ); - const mustInitializeAmmCache = ammCacheAccount?.data == null; - const initializeMarketIxs = await this.getInitializePerpMarketIx( marketIndex, priceOracle, @@ -540,8 +534,7 @@ export class AdminClient extends DriftClient { curveUpdateIntensity, ammJitIntensity, name, - lpPoolId, - mustInitializeAmmCache + lpPoolId ); const tx = await this.buildTransaction(initializeMarketIxs); @@ -588,8 +581,7 @@ export class AdminClient extends DriftClient { curveUpdateIntensity = 0, ammJitIntensity = 0, name = DEFAULT_MARKET_NAME, - lpPoolId: number = 0, - includeInitAmmCacheIx = false + lpPoolId: number = 0 ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, @@ -598,10 +590,6 @@ export class AdminClient extends DriftClient { const ixs: TransactionInstruction[] = []; - if (includeInitAmmCacheIx) { - ixs.push(await this.getInitializeAmmCacheIx()); - } - const nameBuffer = encodeName(name); const initPerpIx = await this.program.instruction.initializePerpMarket( marketIndex, @@ -638,7 +626,6 @@ export class AdminClient extends DriftClient { : this.wallet.publicKey, oracle: priceOracle, perpMarket: perpMarketPublicKey, - ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, @@ -667,8 +654,8 @@ export class AdminClient extends DriftClient { admin: this.useHotWalletAdmin ? this.wallet.publicKey : this.getStateAccount().admin, - ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, + ammCache: getAmmCachePublicKey(this.program.programId), systemProgram: anchor.web3.SystemProgram.programId, }, }); diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index f11d15d6d4..798a9e5713 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4285,11 +4285,6 @@ "isMut": true, "isSigner": false }, - { - "name": "ammCache", - "isMut": true, - "isSigner": false - }, { "name": "oracle", "isMut": false, @@ -4454,7 +4449,7 @@ "args": [] }, { - "name": "resizeAmmCache", + "name": "addMarketToAmmCache", "accounts": [ { "name": "admin", @@ -4471,6 +4466,11 @@ "isMut": true, "isSigner": false }, + { + "name": "perpMarket", + "isMut": false, + "isSigner": false + }, { "name": "rent", "isMut": false, @@ -8478,32 +8478,6 @@ } ] }, - { - "name": "resetAmmCache", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "ammCache", - "isMut": true, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "lpPoolSwap", "accounts": [ @@ -12267,6 +12241,10 @@ "name": "oracleSlot", "type": "u64" }, + { + "name": "marketIndex", + "type": "u16" + }, { "name": "oracleSource", "type": "u8" @@ -12288,7 +12266,7 @@ "type": { "array": [ "u8", - 36 + 34 ] } } @@ -19777,6 +19755,11 @@ "code": 6343, "name": "InvalidLpPoolId", "msg": "Invalid Lp Pool Id for Operation" + }, + { + "code": 6344, + "name": "MarketIndexNotFoundAmmCache", + "msg": "MarketIndexNotFoundAmmCache" } ] } \ No newline at end of file diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 6cd7229776..5c35d73526 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -191,6 +191,8 @@ describe('LP Pool', () => { solUsdLazer = getPythLazerOraclePublicKey(program.programId, 6); await adminClient.initializePythLazerOracle(6); + await adminClient.initializeAmmCache(); + await adminClient.initializePerpMarket( 0, solUsd, @@ -199,6 +201,7 @@ describe('LP Pool', () => { periodicity, new BN(200 * PEG_PRECISION.toNumber()) ); + await adminClient.addMarketToAmmCache(0); await adminClient.updatePerpMarketLpPoolStatus(0, 1); await adminClient.initializePerpMarket( @@ -209,6 +212,7 @@ describe('LP Pool', () => { periodicity, new BN(200 * PEG_PRECISION.toNumber()) ); + await adminClient.addMarketToAmmCache(1); await adminClient.updatePerpMarketLpPoolStatus(1, 1); await adminClient.initializePerpMarket( @@ -219,6 +223,7 @@ describe('LP Pool', () => { periodicity, new BN(200 * PEG_PRECISION.toNumber()) ); + await adminClient.addMarketToAmmCache(2); await adminClient.updatePerpMarketLpPoolStatus(2, 1); await adminClient.updatePerpAuctionDuration(new BN(0)); @@ -270,7 +275,6 @@ describe('LP Pool', () => { new BN(1_000_000).mul(QUOTE_PRECISION), Keypair.generate() ); - await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); // Give the vamm some inventory diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index d256c56ed3..dea540f49e 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -70,7 +70,7 @@ import { dotenv.config(); const NUMBER_OF_CONSTITUENTS = 10; -const NUMBER_OF_PERP_MARKETS = 60; +const NUMBER_OF_PERP_MARKETS = 40; const NUMBER_OF_USERS = Math.ceil(NUMBER_OF_PERP_MARKETS / 8); const PERP_MARKET_INDEXES = Array.from( @@ -255,6 +255,7 @@ describe('LP Pool', () => { new Keypair() ); await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + await adminClient.initializeAmmCache(); // check LpPool created const lpPool = (await adminClient.program.account.lpPool.fetch( @@ -414,6 +415,7 @@ describe('LP Pool', () => { new BN(0), new BN(200 * PEG_PRECISION.toNumber()) ); + await adminClient.addMarketToAmmCache(i); await adminClient.updatePerpMarketLpPoolStatus(i, 1); await adminClient.updatePerpMarketLpPoolFeeTransferScalar(i, 100); await sleep(50); diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 4361c9a948..866436c136 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -176,6 +176,8 @@ describe('LP Pool', () => { const periodicity = new BN(0); + await adminClient.initializeAmmCache(); + await adminClient.initializePerpMarket( 0, spotMarketOracle, @@ -184,6 +186,7 @@ describe('LP Pool', () => { periodicity, new BN(224 * PEG_PRECISION.toNumber()) ); + await adminClient.addMarketToAmmCache(0); await adminClient.updatePerpMarketLpPoolStatus(0, 1); await adminClient.initializePerpMarket( @@ -194,6 +197,7 @@ describe('LP Pool', () => { periodicity, new BN(224 * PEG_PRECISION.toNumber()) ); + await adminClient.addMarketToAmmCache(1); await adminClient.updatePerpMarketLpPoolStatus(1, 1); const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( From ce37116e44c84ae4bb307564f7bd4043376c5421 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:40:08 -0800 Subject: [PATCH 156/159] fix anchor tests --- programs/drift/src/instructions/admin.rs | 9 ++++++++- tests/admin.ts | 1 + tests/placeAndMakeSignedMsgBankrun.ts | 1 + tests/prelisting.ts | 1 + tests/pythLazerBankrun.ts | 1 + tests/switchOracle.ts | 1 + 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index edd3cd69a4..2004129900 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -3812,7 +3812,14 @@ pub fn handle_update_perp_market_oracle( perp_market.amm.oracle = oracle; perp_market.amm.oracle_source = oracle_source; - amm_cache.update_perp_market_fields(perp_market)?; + if amm_cache + .cache + .iter() + .find(|cache_info| cache_info.market_index == perp_market.market_index) + .is_some() + { + amm_cache.update_perp_market_fields(perp_market)?; + } Ok(()) } diff --git a/tests/admin.ts b/tests/admin.ts index c397b3f350..533068d290 100644 --- a/tests/admin.ts +++ b/tests/admin.ts @@ -106,6 +106,7 @@ describe('admin', () => { new BN(1000), periodicity ); + await driftClient.initializeAmmCache(); }); it('checks market name', async () => { diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 5971501251..5cf0368d8d 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -196,6 +196,7 @@ describe('place and make signedMsg order', () => { periodicity, new BN(224 * PEG_PRECISION.toNumber()) ); + await makerDriftClient.initializeAmmCache(); await makerDriftClient.initializeUserAccountAndDepositCollateral( usdcAmount, diff --git a/tests/prelisting.ts b/tests/prelisting.ts index 92f7a510af..ca784e7b53 100644 --- a/tests/prelisting.ts +++ b/tests/prelisting.ts @@ -130,6 +130,7 @@ describe('prelisting', () => { new BN(32 * PEG_PRECISION.toNumber()), OracleSource.Prelaunch ); + await adminDriftClient.initializeAmmCache(); await adminDriftClient.updatePerpMarketBaseSpread( 0, diff --git a/tests/pythLazerBankrun.ts b/tests/pythLazerBankrun.ts index 7e1721bea9..d14ec2f085 100644 --- a/tests/pythLazerBankrun.ts +++ b/tests/pythLazerBankrun.ts @@ -115,6 +115,7 @@ describe('pyth pull oracles', () => { periodicity, new BN(224 * PEG_PRECISION.toNumber()) ); + await driftClient.initializeAmmCache(); await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); }); diff --git a/tests/switchOracle.ts b/tests/switchOracle.ts index 7cd425e8c4..eaefb12f98 100644 --- a/tests/switchOracle.ts +++ b/tests/switchOracle.ts @@ -117,6 +117,7 @@ describe('switch oracles', () => { periodicity, new BN(30 * PEG_PRECISION.toNumber()) ); + await admin.initializeAmmCache(); }); beforeEach(async () => { From 1dffca7040910efa9bf5ef4e5783a2adda647eff Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:49:09 -0800 Subject: [PATCH 157/159] update idl with delete func --- programs/drift/src/lib.rs | 6 ++++++ sdk/src/idl/drift.json | 21 ++++++++++++++++++ tests/lpPool.ts | 45 +++++++++++++++++++++------------------ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 7ec90f959e..70fe980b75 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1026,6 +1026,12 @@ pub mod drift { handle_add_market_to_amm_cache(ctx) } + pub fn delete_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DeleteAmmCache<'info>>, + ) -> Result<()> { + handle_delete_amm_cache(ctx) + } + pub fn update_initial_amm_cache_info<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, ) -> Result<()> { diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 798a9e5713..f8ce96e37a 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4484,6 +4484,27 @@ ], "args": [] }, + { + "name": "deleteAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "updateInitialAmmCacheInfo", "accounts": [ diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 5c35d73526..b82a5623eb 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -1736,25 +1736,28 @@ describe('LP Pool', () => { } }); - // it('can delete amm cache and then init and realloc and update', async () => { - // const ammCacheKey = getAmmCachePublicKey(program.programId); - // const ammCacheBefore = (await adminClient.program.account.ammCache.fetch( - // ammCacheKey - // )) as AmmCache; - - // await adminClient.deleteAmmCache(); - // await adminClient.resizeAmmCache(); - // await adminClient.updateInitialAmmCacheInfo([0, 1, 2]); - // await adminClient.updateAmmCache([0, 1, 2]); - - // const ammCacheAfter = (await adminClient.program.account.ammCache.fetch( - // ammCacheKey - // )) as AmmCache; - - // for (let i = 0; i < ammCacheBefore.cache.length; i++) { - // assert( - // ammCacheBefore.cache[i].position.eq(ammCacheAfter.cache[i].position) - // ); - // } - // }); + it('can delete amm cache and then init and realloc and update', async () => { + const ammCacheKey = getAmmCachePublicKey(program.programId); + const ammCacheBefore = (await adminClient.program.account.ammCache.fetch( + ammCacheKey + )) as AmmCache; + + await adminClient.deleteAmmCache(); + await adminClient.initializeAmmCache(); + await adminClient.addMarketToAmmCache(0); + await adminClient.addMarketToAmmCache(1); + await adminClient.addMarketToAmmCache(2); + await adminClient.updateInitialAmmCacheInfo([0, 1, 2]); + await adminClient.updateAmmCache([0, 1, 2]); + + const ammCacheAfter = (await adminClient.program.account.ammCache.fetch( + ammCacheKey + )) as AmmCache; + + for (let i = 0; i < ammCacheBefore.cache.length; i++) { + assert( + ammCacheBefore.cache[i].position.eq(ammCacheAfter.cache[i].position) + ); + } + }); }); From 507a52fed8619ef76f0f848dff409e7063fdd8be Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 18 Nov 2025 13:42:13 -0500 Subject: [PATCH 158/159] add log --- programs/drift/src/instructions/keeper.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index e9ab5d4c21..b2998bb0ca 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -852,6 +852,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( if let Some(isolated_position_deposit) = verified_message_and_signature.isolated_position_deposit { + msg!("Isolated position deposit: {:?}", isolated_position_deposit); spot_market_map.update_writable_spot_market(0)?; transfer_isolated_perp_position_deposit( taker, From 9a548d7009d87b7b6335815a519be6c315013166 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 18 Nov 2025 14:34:28 -0500 Subject: [PATCH 159/159] fix deposit --- programs/drift/src/instructions/keeper.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index b2998bb0ca..39c2a13950 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -845,6 +845,17 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( return Ok(()); } + // 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 = + SignedMsgOrderId::new(verified_message_and_signature.uuid, max_slot, 0); + if signed_msg_account + .check_exists_and_prune_stale_signed_msg_order_ids(signed_msg_order_id, clock.slot) + { + msg!("SignedMsg order already exists for taker {:?}", taker_key); + return Ok(()); + } + if let Some(max_margin_ratio) = verified_message_and_signature.max_margin_ratio { taker.update_perp_position_max_margin_ratio(market_index, max_margin_ratio)?; } @@ -868,17 +879,6 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( )?; } - // 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 = - SignedMsgOrderId::new(verified_message_and_signature.uuid, max_slot, 0); - if signed_msg_account - .check_exists_and_prune_stale_signed_msg_order_ids(signed_msg_order_id, clock.slot) - { - msg!("SignedMsg order already exists for taker {:?}", taker_key); - return Ok(()); - } - // Good to place orders, do stop loss and take profit orders first if let Some(stop_loss_order_params) = verified_message_and_signature.stop_loss_order_params { taker_order_id_to_use += 1;