diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 1479c28ead..7dd593ebb6 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -665,8 +665,8 @@ fn amm_perp_ref_offset() { max_ref_offset, ) .unwrap(); - assert_eq!(res, 18000); - assert_eq!(perp_market.amm.reference_price_offset, 18000); + assert_eq!(res, 45000); + assert_eq!(perp_market.amm.reference_price_offset, 18000); // not updated vs market account let now = 1741207620 + 1; let clock_slot = 324817761 + 1; // todo @@ -696,12 +696,14 @@ fn amm_perp_ref_offset() { perp_market.amm.historical_oracle_data.last_oracle_price, 7101600 ); - assert_eq!(perp_market.amm.reference_price_offset, 18000); + assert_eq!(perp_market.amm.reference_price_offset, 45000); assert_eq!(perp_market.amm.max_spread, 90000); assert_eq!(r, 7101599); - assert_eq!(perp_market.amm.bid_base_asset_reserve, 4633657972174584); - assert_eq!(perp_market.amm.ask_base_asset_reserve, 4631420570932586); + assert_eq!(perp_market.amm.bid_base_asset_reserve, 4570430670410018); + assert_eq!(perp_market.amm.ask_base_asset_reserve, 4568069910766211); + + crate::validation::perp_market::validate_perp_market(&perp_market).unwrap(); } #[test] diff --git a/programs/drift/src/math/amm_spread/tests.rs b/programs/drift/src/math/amm_spread/tests.rs index 1d983ff1af..ff1059f1c3 100644 --- a/programs/drift/src/math/amm_spread/tests.rs +++ b/programs/drift/src/math/amm_spread/tests.rs @@ -394,9 +394,9 @@ mod test { let max_ref_offset = market.amm.get_max_reference_price_offset().unwrap(); assert_eq!(max_ref_offset, 10000); // 100 bps - market.amm.max_spread = 10000 * 10; // 10% + market.amm.max_spread = 10000 * 5; // 5% let max_ref_offset = market.amm.get_max_reference_price_offset().unwrap(); - assert_eq!(max_ref_offset, 20000); // 200 bps (5% of max spread) + assert_eq!(max_ref_offset, 25000); // 250 bps (5% of max spread) let orig_price = calculate_price( amm.quote_asset_reserve, diff --git a/programs/drift/src/math/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index 9103c3b5b1..6f3e207afa 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -71,7 +71,8 @@ fn calculate_k_tests_with_spread() { market.amm.bid_base_asset_reserve >= market.amm.base_asset_reserve && market.amm.bid_quote_asset_reserve <= market.amm.quote_asset_reserve, ErrorCode::InvalidAmmDetected, - "bid reserves out of wack: {} -> {}, quote: {} -> {}", + "market index {} amm bid reserves invalid: {} -> {}, quote: {} -> {}", + market.market_index, market.amm.bid_base_asset_reserve, market.amm.base_asset_reserve, market.amm.bid_quote_asset_reserve, 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..d722bd17d9 --- /dev/null +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -0,0 +1,181 @@ +use super::*; +use crate::math::constants::PERCENTAGE_PRECISION_U64; +use crate::state::oracle::OracleSource; + +fn weight_datum(data: u64, last_slot: u64) -> WeightDatum { + WeightDatum { data, last_slot } +} + +fn dummy_constituent(index: u16) -> Constituent { + Constituent { + pubkey: Pubkey::default(), + constituent_index: index, + oracle: Pubkey::default(), + oracle_source: OracleSource::Pyth, + max_weight_deviation: 0, + swap_fee_min: 0, + max_fee_premium: 0, + spot_market_index: index, + last_oracle_price: 0, + last_oracle_price_ts: 0, + spot_balance: BLPosition { + scaled_balance: 0, + cumulative_deposits: 0, + market_index: index, + balance_type: SpotBalanceType::Deposit, + padding: [0; 4], + }, + } +} + +#[test] +fn test_single_zero_weight() { + let mut mapping = AmmConstituentMapping { + num_rows: 1, + num_cols: 1, + data: vec![weight_datum(0, 0)], + }; + + let amm_inventory = vec![1_000_000u64]; // 1 unit + let prices = vec![1_000_000u64]; // price = 1.0 + let constituents = vec![dummy_constituent(0)]; + let aum = 1_000_000; // 1 USD + let now_ts = 1000; + + let mut target = ConstituentTargetWeights { + num_rows: 0, + num_cols: 0, + oldest_weight_ts: 0, + data: vec![], + }; + + target.update_target_weights( + &mapping, + &amm_inventory, + &constituents, + &prices, + aum, + now_ts, + ); + + assert_eq!(target.data.len(), 1); + assert_eq!(target.data[0].data, 0); + assert_eq!(target.data[0].last_slot, now_ts); +} + +#[test] +fn test_single_full_weight() { + let mut mapping = AmmConstituentMapping { + num_rows: 1, + num_cols: 1, + data: vec![weight_datum(PERCENTAGE_PRECISION_U64, 0)], + }; + + let amm_inventory = vec![1_000_000]; + let prices = vec![1_000_000]; // price = 1.0 + let constituents = vec![dummy_constituent(0)]; + let aum = 1_000_000; // 1 USD + let now_ts = 1234; + + let mut target = ConstituentTargetWeights::default(); + target.update_target_weights( + &mapping, + &amm_inventory, + &constituents, + &prices, + aum, + now_ts, + ); + + assert_eq!(target.data.len(), 1); + assert_eq!(target.data[0].data, PERCENTAGE_PRECISION_U64); // 100% + assert_eq!(target.data[0].last_slot, now_ts); +} + +#[test] +fn test_multiple_constituents_partial_weights() { + let mut mapping = AmmConstituentMapping { + num_rows: 1, + num_cols: 2, + data: vec![ + weight_datum(PERCENTAGE_PRECISION_U64 / 2, 0), + weight_datum(PERCENTAGE_PRECISION_U64 / 2, 0), + ], + }; + + let amm_inventory = vec![1_000_000]; + let prices = vec![1_000_000, 1_000_000]; + let constituents = vec![dummy_constituent(0), dummy_constituent(1)]; + let aum = 1_000_000; + let now_ts = 999; + + let mut target = ConstituentTargetWeights::default(); + target.update_target_weights( + &mapping, + &amm_inventory, + &constituents, + &prices, + aum, + now_ts, + ); + + assert_eq!(target.data.len(), 2); + assert_eq!(target.data[0].data, PERCENTAGE_PRECISION_U64 / 2); + assert_eq!(target.data[1].data, PERCENTAGE_PRECISION_U64 / 2); +} + +#[test] +fn test_zero_aum_safe() { + let mut mapping = AmmConstituentMapping { + num_rows: 1, + num_cols: 1, + data: vec![weight_datum(PERCENTAGE_PRECISION_U64, 0)], + }; + + let amm_inventory = vec![1_000_000]; + let prices = vec![1_000_000]; + let constituents = vec![dummy_constituent(0)]; + let aum = 0; + let now_ts = 111; + + let mut target = ConstituentTargetWeights::default(); + target.update_target_weights( + &mapping, + &amm_inventory, + &constituents, + &prices, + aum, + now_ts, + ); + + assert_eq!(target.data.len(), 2); + assert_eq!(target.data[0].data, 0); // No division by zero panic +} + +#[test] +fn test_overflow_protection() { + let mut mapping = AmmConstituentMapping { + num_rows: 1, + num_cols: 1, + data: vec![weight_datum(u64::MAX, 0)], + }; + + let amm_inventory = vec![u64::MAX]; + let prices = vec![u64::MAX]; + let constituents = vec![dummy_constituent(0)]; + let aum = 1; // smallest possible AUM to maximize weight + let now_ts = 222; + + let mut target = ConstituentTargetWeights::default(); + target.update_target_weights( + &mapping, + &amm_inventory, + &constituents, + &prices, + aum, + now_ts, + ); + + assert_eq!(target.data.len(), 2); + assert!(target.data[0].data <= PERCENTAGE_PRECISION_U64); // cap at max +} diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 8671275356..869c22af13 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1210,10 +1210,10 @@ impl AMM { let lower_bound_multiplier: i64 = self.curve_update_intensity.safe_sub(100)?.cast::()?; - // always allow 1-100 bps of price offset, up to a fifth of the market's max_spread + // always allow 1-100 bps of price offset, up to half of the market's max_spread let lb_bps = (PERCENTAGE_PRECISION.cast::()? / 10000).safe_mul(lower_bound_multiplier)?; - let max_offset = (self.max_spread.cast::()? / 5).max(lb_bps); + let max_offset = (self.max_spread.cast::()? / 2).max(lb_bps); Ok(max_offset) } diff --git a/programs/drift/src/validation/perp_market.rs b/programs/drift/src/validation/perp_market.rs index 781cee4603..27ab686323 100644 --- a/programs/drift/src/validation/perp_market.rs +++ b/programs/drift/src/validation/perp_market.rs @@ -25,7 +25,8 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { validate!( remainder_base_asset_amount_long == 0 && remainder_base_asset_amount_short == 0, ErrorCode::InvalidPositionDelta, - "invalid base_asset_amount_long/short vs order_step_size, remainder={}/{}", + "market {} invalid base_asset_amount_long/short vs order_step_size, remainder={}/{}", + market.market_index, remainder_base_asset_amount_short, market.amm.order_step_size )?; @@ -49,21 +50,24 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { validate!( market.amm.base_asset_amount_with_amm <= (MAX_BASE_ASSET_AMOUNT_WITH_AMM as i128), ErrorCode::InvalidAmmDetected, - "market.amm.base_asset_amount_with_amm={} is too large", + "market {} market.amm.base_asset_amount_with_amm={} is too large", + market.market_index, market.amm.base_asset_amount_with_amm )?; validate!( market.amm.peg_multiplier > 0, ErrorCode::InvalidAmmDetected, - "peg_multiplier out of wack" + "market {} peg_multiplier out of wack", + market.market_index, )?; if market.status != MarketStatus::ReduceOnly { validate!( market.amm.sqrt_k > market.amm.base_asset_amount_with_amm.unsigned_abs(), ErrorCode::InvalidAmmDetected, - "k out of wack: k={}, net_baa={}", + "market {} k out of wack: k={}, net_baa={}", + market.market_index, market.amm.sqrt_k, market.amm.base_asset_amount_with_amm )?; @@ -73,7 +77,8 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.amm.sqrt_k >= market.amm.base_asset_reserve || market.amm.sqrt_k >= market.amm.quote_asset_reserve, ErrorCode::InvalidAmmDetected, - "k out of wack: k={}, bar={}, qar={}", + "market {} k out of wack: k={}, bar={}, qar={}", + market.market_index, market.amm.sqrt_k, market.amm.base_asset_reserve, market.amm.quote_asset_reserve @@ -82,7 +87,8 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { validate!( market.amm.sqrt_k >= market.amm.user_lp_shares, ErrorCode::InvalidAmmDetected, - "market.amm.sqrt_k < market.amm.user_lp_shares: {} < {}", + "market {} market.amm.sqrt_k < market.amm.user_lp_shares: {} < {}", + market.market_index, market.amm.sqrt_k, market.amm.user_lp_shares, )?; @@ -101,7 +107,8 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { validate!( rounding_diff <= 15, ErrorCode::InvalidAmmDetected, - "qar/bar/k out of wack: k={}, bar={}, qar={}, qar'={} (rounding: {})", + "market {} amm qar/bar/k invalid: k={}, bar={}, qar={}, qar'={} (rounding: {})", + market.market_index, invariant, market.amm.base_asset_reserve, market.amm.quote_asset_reserve, @@ -112,34 +119,41 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { // todo if market.amm.base_spread > 0 { // bid quote/base < reserve q/b - validate!( - market.amm.bid_base_asset_reserve >= market.amm.base_asset_reserve - && market.amm.bid_quote_asset_reserve <= market.amm.quote_asset_reserve, - ErrorCode::InvalidAmmDetected, - "bid reserves out of wack: {} -> {}, quote: {} -> {}", - market.amm.bid_base_asset_reserve, - market.amm.base_asset_reserve, - market.amm.bid_quote_asset_reserve, - market.amm.quote_asset_reserve - )?; + if market.amm.reference_price_offset <= 0 { + validate!( + market.amm.bid_base_asset_reserve >= market.amm.base_asset_reserve + && market.amm.bid_quote_asset_reserve <= market.amm.quote_asset_reserve, + ErrorCode::InvalidAmmDetected, + "market {} amm bid reserves invalid: {} -> {}, quote: {} -> {}", + market.market_index, + market.amm.bid_base_asset_reserve, + market.amm.base_asset_reserve, + market.amm.bid_quote_asset_reserve, + market.amm.quote_asset_reserve + )?; + } - // ask quote/base > reserve q/b - validate!( - market.amm.ask_base_asset_reserve <= market.amm.base_asset_reserve - && market.amm.ask_quote_asset_reserve >= market.amm.quote_asset_reserve, - ErrorCode::InvalidAmmDetected, - "ask reserves out of wack base: {} -> {}, quote: {} -> {}", - market.amm.ask_base_asset_reserve, - market.amm.base_asset_reserve, - market.amm.ask_quote_asset_reserve, - market.amm.quote_asset_reserve - )?; + if market.amm.reference_price_offset >= 0 { + // ask quote/base > reserve q/b + validate!( + market.amm.ask_base_asset_reserve <= market.amm.base_asset_reserve + && market.amm.ask_quote_asset_reserve >= market.amm.quote_asset_reserve, + ErrorCode::InvalidAmmDetected, + "market {} amm ask reserves invalid: {} -> {}, quote: {} -> {}", + market.market_index, + market.amm.ask_base_asset_reserve, + market.amm.base_asset_reserve, + market.amm.ask_quote_asset_reserve, + market.amm.quote_asset_reserve + )?; + } } validate!( market.amm.long_spread + market.amm.short_spread >= market.amm.base_spread, ErrorCode::InvalidAmmDetected, - "long_spread + short_spread < base_spread: {} + {} < {}", + "market {} amm long_spread + short_spread < base_spread: {} + {} < {}", + market.market_index, market.amm.long_spread, market.amm.short_spread, market.amm.base_spread @@ -153,7 +167,8 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { .cast::()? <= BID_ASK_SPREAD_PRECISION, ErrorCode::InvalidAmmDetected, - "long_spread {} + short_spread {} > max bid-ask spread precision (max spread = {})", + "market {} amm long_spread {} + short_spread {} > max bid-ask spread precision (max spread = {})", + market.market_index, market.amm.long_spread, market.amm.short_spread, market.amm.max_spread, @@ -165,13 +180,15 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { validate!( market.amm.terminal_quote_asset_reserve <= market.amm.quote_asset_reserve, ErrorCode::InvalidAmmDetected, - "terminal_quote_asset_reserve out of wack" + "market {} terminal_quote_asset_reserve out of wack", + market.market_index, )?; } else if market.amm.base_asset_amount_with_amm < 0 { validate!( market.amm.terminal_quote_asset_reserve >= market.amm.quote_asset_reserve, ErrorCode::InvalidAmmDetected, - "terminal_quote_asset_reserve out of wack (terminal <) {} > {}", + "market {} terminal_quote_asset_reserve out of wack (terminal <) {} > {}", + market.market_index, market.amm.terminal_quote_asset_reserve, market.amm.quote_asset_reserve )?; @@ -179,7 +196,8 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { validate!( market.amm.terminal_quote_asset_reserve == market.amm.quote_asset_reserve, ErrorCode::InvalidAmmDetected, - "terminal_quote_asset_reserve out of wack {}!={}", + "market {} terminal_quote_asset_reserve out of wack {}!={}", + market.market_index, market.amm.terminal_quote_asset_reserve, market.amm.quote_asset_reserve )?; @@ -190,7 +208,8 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.amm.max_spread > market.amm.base_spread && market.amm.max_spread < market.margin_ratio_initial * 100, ErrorCode::InvalidAmmDetected, - "invalid max_spread", + "market {} amm invalid max_spread", + market.market_index, )?; } @@ -198,9 +217,10 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { .insurance_claim .max_revenue_withdraw_per_period >= market.insurance_claim.revenue_withdraw_since_last_settle.unsigned_abs(), ErrorCode::InvalidAmmDetected, - "market + "{} market .insurance_claim .max_revenue_withdraw_per_period={} < |market.insurance_claim.revenue_withdraw_since_last_settle|={}", + market.market_index, market .insurance_claim .max_revenue_withdraw_per_period, @@ -210,14 +230,16 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { 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.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.amm.quote_asset_amount_per_lp too large: {}", + market.market_index, market.amm.quote_asset_amount_per_lp )?;