diff --git a/programs/clearing_house/src/context.rs b/programs/clearing_house/src/context.rs index 23b86782..19976a77 100644 --- a/programs/clearing_house/src/context.rs +++ b/programs/clearing_house/src/context.rs @@ -851,6 +851,11 @@ pub struct UpdateFundingRate<'info> { constraint = &state.funding_rate_history.eq(&funding_rate_history.key()) )] pub funding_rate_history: AccountLoader<'info, FundingRateHistory>, + #[account( + mut, + constraint = &state.extended_curve_history.eq(&extended_curve_history.key()) + )] + pub extended_curve_history: AccountLoader<'info, ExtendedCurveHistory>, } #[derive(Accounts)] diff --git a/programs/clearing_house/src/controller/amm.rs b/programs/clearing_house/src/controller/amm.rs index d136c105..8cb718d2 100644 --- a/programs/clearing_house/src/controller/amm.rs +++ b/programs/clearing_house/src/controller/amm.rs @@ -1,12 +1,19 @@ use solana_program::msg; +use crate::controller::repeg::apply_cost_to_market; use crate::error::{ClearingHouseResult, ErrorCode}; -use crate::math::amm::calculate_quote_asset_amount_swapped; -use crate::math::casting::{cast, cast_to_i128}; +use crate::math::amm::{calculate_quote_asset_amount_swapped, get_update_k_result}; +use crate::math::casting::{cast, cast_to_i128, cast_to_i64}; use crate::math::constants::PRICE_TO_PEG_PRECISION_RATIO; -use crate::math::{amm, bn, quote_asset::*}; +use crate::math::{amm, bn, quote_asset::*, repeg}; use crate::math_error; -use crate::state::market::AMM; +use crate::state::history::curve::{ExtendedCurveHistory, ExtendedCurveRecord}; +use crate::state::market::{Market, OraclePriceData, AMM}; +use std::cell::RefMut; + +use anchor_lang::AccountLoader; +use solana_program::log::sol_log_compute_units; +use std::cmp::{max, min}; #[derive(Clone, Copy, PartialEq)] pub enum SwapDirection { @@ -92,6 +99,101 @@ pub fn move_price( Ok(()) } +pub fn formulaic_update_k( + market: &mut Market, + oracle_price_data: &OraclePriceData, + funding_imbalance_cost: i128, + curve_history: Option<&AccountLoader>, + now: i64, + market_index: u64, + trade_record: Option, + mark_price: u128, +) -> ClearingHouseResult { + let peg_multiplier_before = market.amm.peg_multiplier; + let base_asset_reserve_before = market.amm.base_asset_reserve; + let quote_asset_reserve_before = market.amm.quote_asset_reserve; + let sqrt_k_before = market.amm.sqrt_k; + + let funding_imbalance_cost_i64 = cast_to_i64(funding_imbalance_cost)?; + + // calculate budget + let budget = if funding_imbalance_cost_i64 < 0 { + // negative cost is period revenue, give back half in k increase + funding_imbalance_cost_i64 + .checked_div(2) + .ok_or_else(math_error!())? + .abs() + } else if market.amm.net_revenue_since_last_funding < funding_imbalance_cost_i64 { + // cost exceeded period revenue, take back half in k decrease + max(0, market.amm.net_revenue_since_last_funding) + .checked_sub(funding_imbalance_cost_i64) + .ok_or_else(math_error!())? + .checked_div(2) + .ok_or_else(math_error!())? + } else { + 0 + }; + + if budget != 0 && curve_history.is_some() { + let curve_history = &mut curve_history + .unwrap() + .load_mut() + .or(Err(ErrorCode::UnableToLoadAccountLoader))?; + + // single k scale is capped by .1% increase and .09% decrease (regardless of budget) + let (k_scale_numerator, k_scale_denominator) = + amm::calculate_budgeted_k_scale(market, cast_to_i128(budget)?, mark_price)?; + + let new_sqrt_k = bn::U192::from(market.amm.sqrt_k) + .checked_mul(bn::U192::from(k_scale_numerator)) + .ok_or_else(math_error!())? + .checked_div(bn::U192::from(k_scale_denominator)) + .ok_or_else(math_error!())?; + + let update_k_result = get_update_k_result(market, new_sqrt_k)?; + + let adjustment_cost = amm::adjust_k_cost(market, &update_k_result)?; + + let cost_applied = apply_cost_to_market(market, adjustment_cost)?; + + if cost_applied { + // todo: do actual k adj here + amm::update_k(market, &update_k_result)?; + + let peg_multiplier_after = market.amm.peg_multiplier; + let base_asset_reserve_after = market.amm.base_asset_reserve; + let quote_asset_reserve_after = market.amm.quote_asset_reserve; + let sqrt_k_after = market.amm.sqrt_k; + + let record_id = curve_history.next_record_id(); + curve_history.append(ExtendedCurveRecord { + ts: now, + record_id, + market_index, + peg_multiplier_before, + base_asset_reserve_before, + quote_asset_reserve_before, + sqrt_k_before, + peg_multiplier_after, + base_asset_reserve_after, + quote_asset_reserve_after, + sqrt_k_after, + base_asset_amount_long: market.base_asset_amount_long.unsigned_abs(), + base_asset_amount_short: market.base_asset_amount_short.unsigned_abs(), + base_asset_amount: market.base_asset_amount, + open_interest: market.open_interest, + total_fee: market.amm.total_fee, + total_fee_minus_distributions: market.amm.total_fee_minus_distributions, + adjustment_cost, + oracle_price: oracle_price_data.price, + trade_record: trade_record.unwrap_or(0), + padding: [0; 5], + }); + } + } + Ok(()) +} + #[allow(dead_code)] pub fn move_to_price(amm: &mut AMM, target_price: u128) -> ClearingHouseResult { let sqrt_k = bn::U256::from(amm.sqrt_k); diff --git a/programs/clearing_house/src/controller/funding.rs b/programs/clearing_house/src/controller/funding.rs index d1dd9e1c..b18eb104 100644 --- a/programs/clearing_house/src/controller/funding.rs +++ b/programs/clearing_house/src/controller/funding.rs @@ -4,16 +4,22 @@ use std::cmp::{max, min}; use anchor_lang::prelude::*; use crate::error::*; + +use crate::controller::amm::formulaic_update_k; + use crate::math::amm; use crate::math::amm::normalise_oracle_price; use crate::math::casting::{cast, cast_to_i128}; use crate::math::collateral::calculate_updated_collateral; use crate::math::constants::{ - AMM_TO_QUOTE_PRECISION_RATIO_I128, FUNDING_PAYMENT_PRECISION, ONE_HOUR, TWENTYFOUR_HOUR, + AMM_RESERVE_PRECISION_I128, AMM_TO_QUOTE_PRECISION_RATIO, AMM_TO_QUOTE_PRECISION_RATIO_I128, + FUNDING_PAYMENT_PRECISION, FUNDING_RATE_PRECISION_I128, ONE_HOUR, + QUOTE_TO_BASE_AMT_FUNDING_PRECISION, TWENTYFOUR_HOUR, }; use crate::math::funding::{calculate_funding_payment, calculate_funding_rate_long_short}; use crate::math::oracle; use crate::math_error; +use crate::state::history::curve::ExtendedCurveHistory; use crate::state::history::funding_payment::{FundingPaymentHistory, FundingPaymentRecord}; use crate::state::history::funding_rate::{FundingRateHistory, FundingRateRecord}; use crate::state::market::AMM; @@ -21,6 +27,7 @@ use crate::state::market::{Market, Markets}; use crate::state::state::OracleGuardRails; use crate::state::user::{User, UserPositions}; use solana_program::clock::UnixTimestamp; +use solana_program::log::sol_log_compute_units; use solana_program::msg; /// Funding payments are settled lazily. The amm tracks its cumulative funding rate (for longs and shorts) @@ -93,24 +100,31 @@ pub fn update_funding_rate( now: UnixTimestamp, clock_slot: u64, funding_rate_history: &mut RefMut, + curve_history: Option<&AccountLoader>, guard_rails: &OracleGuardRails, funding_paused: bool, precomputed_mark_price: Option, + trade_record_id: Option, ) -> ClearingHouseResult { let time_since_last_update = now .checked_sub(market.amm.last_funding_rate_ts) .ok_or_else(math_error!())?; + let mark_price = match precomputed_mark_price { + Some(mark_price) => mark_price, + None => market.amm.mark_price()?, + }; + // Pause funding if oracle is invalid or if mark/oracle spread is too divergent let (block_funding_rate_update, oracle_price_data) = oracle::block_operation( &market.amm, price_oracle, clock_slot, guard_rails, - precomputed_mark_price, + Some(mark_price), )?; let normalised_oracle_price = - normalise_oracle_price(&market.amm, &oracle_price_data, precomputed_mark_price)?; + normalise_oracle_price(&market.amm, &oracle_price_data, Some(mark_price))?; // round next update time to be available on the hour let mut next_update_wait = market.amm.funding_period; @@ -146,9 +160,17 @@ pub fn update_funding_rate( } if !funding_paused && !block_funding_rate_update && time_since_last_update >= next_update_wait { - let oracle_price_twap = - amm::update_oracle_price_twap(&mut market.amm, now, normalised_oracle_price)?; - let mark_price_twap = amm::update_mark_twap(&mut market.amm, now, None)?; + let oracle_price_twap = if market.amm.last_oracle_price_twap_ts != now { + amm::update_oracle_price_twap(&mut market.amm, now, normalised_oracle_price)? + } else { + market.amm.last_oracle_price_twap + }; + + let mark_price_twap = if market.amm.last_mark_price_twap_ts != now { + amm::update_mark_twap(&mut market.amm, now, Some(mark_price))? + } else { + market.amm.last_mark_price_twap + }; let period_adjustment = TWENTYFOUR_HOUR .checked_div(max(ONE_HOUR, market.amm.funding_period)) @@ -171,9 +193,20 @@ pub fn update_funding_rate( .checked_div(cast(period_adjustment)?) .ok_or_else(math_error!())?; - let (funding_rate_long, funding_rate_short) = + let (funding_rate_long, funding_rate_short, funding_imbalance_cost) = calculate_funding_rate_long_short(market, funding_rate)?; + formulaic_update_k( + market, + &oracle_price_data, + funding_imbalance_cost, + curve_history, + now, + market_index, + trade_record_id, + mark_price, + )?; + market.amm.cumulative_funding_rate_long = market .amm .cumulative_funding_rate_long diff --git a/programs/clearing_house/src/controller/orders.rs b/programs/clearing_house/src/controller/orders.rs index ce7ff9c2..470bf09f 100644 --- a/programs/clearing_house/src/controller/orders.rs +++ b/programs/clearing_house/src/controller/orders.rs @@ -807,17 +807,19 @@ pub fn fill_order( now, clock_slot, funding_rate_history, + Some(extended_curve_history), &state.oracle_guard_rails, state.funding_paused, - Some(mark_price_before), + Some(mark_price_after), + Some(trade_record_id), )?; - // if market_index >= 12 { - // todo for soft launch let extended_curve_history = &mut extended_curve_history .load_mut() .or(Err(ErrorCode::UnableToLoadAccountLoader))?; + // if market_index >= 12 { + // todo for soft launch controller::repeg::formulaic_repeg( market, mark_price_after, diff --git a/programs/clearing_house/src/controller/repeg.rs b/programs/clearing_house/src/controller/repeg.rs index ad27add0..d87a446b 100644 --- a/programs/clearing_house/src/controller/repeg.rs +++ b/programs/clearing_house/src/controller/repeg.rs @@ -31,19 +31,14 @@ pub fn repeg( let (repegged_market, adjustment_cost) = repeg::adjust_peg_cost(market, new_peg_candidate)?; - let ( - oracle_is_valid, - direction_valid, - profitability_valid, - price_impact_valid, - _oracle_terminal_divergence, - ) = repeg::calculate_repeg_validity_from_oracle_account( - &repegged_market, - price_oracle, - terminal_price_before, - clock_slot, - oracle_guard_rails, - )?; + let (oracle_is_valid, direction_valid, profitability_valid, price_impact_valid) = + repeg::calculate_repeg_validity_from_oracle_account( + &repegged_market, + price_oracle, + terminal_price_before, + clock_slot, + oracle_guard_rails, + )?; // cannot repeg if oracle is invalid if !oracle_is_valid { @@ -89,9 +84,9 @@ pub fn formulaic_repeg( ) -> ClearingHouseResult { // backrun market swaps to do automatic on-chain repeg - if !is_oracle_valid || oracle_price_data.delay > 5 { + if !is_oracle_valid { msg!( - "invalid oracle (oracle delay = {:?})", + "skipping formulaic_repeg: invalid oracle (oracle delay = {:?})", oracle_price_data.delay ); return Ok(0); @@ -115,22 +110,17 @@ pub fn formulaic_repeg( terminal_quote_reserves, repeg_budget, mark_price, - cast_to_u128(oracle_price_data.price)?, - )?; - - let ( - oracle_valid, - _direction_valid, - profitability_valid, - price_impact_valid, - _oracle_terminal_divergence_pct_after, - ) = repeg::calculate_repeg_validity( - &repegged_market, oracle_price_data, - is_oracle_valid, - terminal_price_before, )?; + let (oracle_valid, _direction_valid, profitability_valid, price_impact_valid) = + repeg::calculate_repeg_validity( + &repegged_market, + oracle_price_data, + is_oracle_valid, + terminal_price_before, + )?; + // any budgeted direction valid for formulaic if oracle_valid && profitability_valid && price_impact_valid { let cost_applied = apply_cost_to_market(market, adjustment_cost)?; @@ -172,7 +162,7 @@ pub fn formulaic_repeg( Ok(adjustment_cost) } -fn apply_cost_to_market(market: &mut Market, cost: i128) -> ClearingHouseResult { +pub fn apply_cost_to_market(market: &mut Market, cost: i128) -> ClearingHouseResult { // positive cost is expense, negative cost is revenue // Reduce pnl to quote asset precision and take the absolute value if cost > 0 { diff --git a/programs/clearing_house/src/error.rs b/programs/clearing_house/src/error.rs index 3eab0fc9..304a543f 100644 --- a/programs/clearing_house/src/error.rs +++ b/programs/clearing_house/src/error.rs @@ -128,6 +128,8 @@ pub enum ErrorCode { CantExpireOrders, #[msg("AMM repeg mark price impact vs oracle too large")] InvalidRepegPriceImpact, + #[msg("Could not deserialize curve history")] + CouldNotDeserializeCurveHistory, } #[macro_export] diff --git a/programs/clearing_house/src/lib.rs b/programs/clearing_house/src/lib.rs index f7ca7356..d6312e61 100644 --- a/programs/clearing_house/src/lib.rs +++ b/programs/clearing_house/src/lib.rs @@ -35,7 +35,7 @@ declare_id!("AsW7LnXB9UA1uec9wi9MctYTgTz7YH9snhxd16GsFaGX"); pub mod clearing_house { use crate::math; use crate::optional_accounts::{ - get_discount_token, get_oracle_for_cancel_order_by_order_id, + get_discount_token, get_extended_curve_history, get_oracle_for_cancel_order_by_order_id, get_oracle_for_cancel_order_by_user_order_id, get_oracle_for_place_order, get_referrer, get_referrer_for_fill_order, }; @@ -46,7 +46,8 @@ pub mod clearing_house { use super::*; use crate::margin_validation::validate_margin; use crate::math::amm::{ - calculate_mark_twap_spread_pct, is_oracle_mark_too_divergent, normalise_oracle_price, + calculate_mark_twap_spread_pct, get_update_k_result, is_oracle_mark_too_divergent, + normalise_oracle_price, }; use crate::math::casting::{cast, cast_to_i128, cast_to_u128}; use crate::math::slippage::{calculate_slippage, calculate_slippage_pct}; @@ -340,8 +341,12 @@ pub mod clearing_house { last_oracle_price: oracle_price, minimum_base_asset_trade_size: 10000000, net_revenue_since_last_funding: 0, + curve_update_intensity: 0, padding2: 0, padding3: 0, + padding4: 0, + padding5: 0, + padding6: 0, }, }; @@ -758,6 +763,12 @@ pub mod clearing_house { [Markets::index_from_u64(market_index)]; let price_oracle = &ctx.accounts.oracle; let funding_rate_history = &mut ctx.accounts.funding_rate_history.load_mut()?; + let extended_curve_history_value = get_extended_curve_history( + ctx.remaining_accounts, + &ctx.accounts.state.extended_curve_history, + )?; + let extended_curve_history_ref = extended_curve_history_value.as_ref(); + controller::funding::update_funding_rate( market_index, market, @@ -765,10 +776,16 @@ pub mod clearing_house { now, clock_slot, funding_rate_history, + extended_curve_history_ref, &ctx.accounts.state.oracle_guard_rails, ctx.accounts.state.funding_paused, - Some(mark_price_before), + Some(mark_price_after), + Some(record_id), )?; + + if let Some(extended_curve_history) = extended_curve_history_value { + extended_curve_history.exit(ctx.program_id); + } } Ok(()) @@ -950,6 +967,12 @@ pub mod clearing_house { oracle_price: oracle_price_after, }); + let extended_curve_history_value = get_extended_curve_history( + ctx.remaining_accounts, + &ctx.accounts.state.extended_curve_history, + )?; + let extended_curve_history_ref = extended_curve_history_value.as_ref(); + // Try to update the funding rate at the end of every trade let funding_rate_history = &mut ctx.accounts.funding_rate_history.load_mut()?; controller::funding::update_funding_rate( @@ -959,11 +982,17 @@ pub mod clearing_house { now, clock_slot, funding_rate_history, + extended_curve_history_ref, &ctx.accounts.state.oracle_guard_rails, ctx.accounts.state.funding_paused, - Some(mark_price_before), + Some(mark_price_after), + Some(record_id), )?; + if let Some(extended_curve_history) = extended_curve_history_value { + extended_curve_history.exit(ctx.program_id); + } + Ok(()) } @@ -2086,9 +2115,11 @@ pub mod clearing_house { now, clock_slot, funding_rate_history, + Some(&ctx.accounts.extended_curve_history), &ctx.accounts.state.oracle_guard_rails, ctx.accounts.state.funding_paused, None, + None, )?; Ok(()) @@ -2123,7 +2154,13 @@ pub mod clearing_house { let quote_asset_reserve_before = market.amm.quote_asset_reserve; let sqrt_k_before = market.amm.sqrt_k; - let adjustment_cost = math::amm::adjust_k_cost(market, bn::U256::from(sqrt_k))?; + let new_sqrt_k_u192 = bn::U192::from(sqrt_k); + + let update_k_result = get_update_k_result(market, new_sqrt_k_u192)?; + + let adjustment_cost = math::amm::adjust_k_cost(market, &update_k_result)?; + + math::amm::update_k(market, &update_k_result); if adjustment_cost > 0 { let max_cost = market @@ -2271,6 +2308,20 @@ pub mod clearing_house { Ok(()) } + #[access_control( + market_initialized(&ctx.accounts.markets, market_index) + )] + pub fn update_curve_update_intensity( + ctx: Context, + market_index: u64, + curve_update_intensity: u8, + ) -> ProgramResult { + let market = + &mut ctx.accounts.markets.load_mut()?.markets[Markets::index_from_u64(market_index)]; + market.amm.curve_update_intensity = curve_update_intensity; + Ok(()) + } + #[access_control( market_initialized(&ctx.accounts.markets, market_index) )] diff --git a/programs/clearing_house/src/math/amm.rs b/programs/clearing_house/src/math/amm.rs index 9142f83d..ff0dd2cf 100644 --- a/programs/clearing_house/src/math/amm.rs +++ b/programs/clearing_house/src/math/amm.rs @@ -7,16 +7,21 @@ use crate::controller::position::PositionDirection; use crate::error::*; use crate::math::bn; use crate::math::bn::U192; +use crate::math::bn_operations::{multiply_i128, multiply_u128}; use crate::math::casting::{cast, cast_to_i128, cast_to_u128}; use crate::math::constants::{ - MARK_PRICE_PRECISION, PEG_PRECISION, PRICE_SPREAD_PRECISION, PRICE_SPREAD_PRECISION_U128, - PRICE_TO_PEG_PRECISION_RATIO, + AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, AMM_TO_QUOTE_PRECISION_RATIO, + AMM_TO_QUOTE_PRECISION_RATIO_I128, K_BPS_DECREASE_MAX, K_BPS_INCREASE_MAX, K_BPS_UPDATE_SCALE, + MARK_PRICE_PRECISION, MARK_PRICE_PRECISION_I128, + MARK_PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, PEG_PRECISION, PRICE_SPREAD_PRECISION, + PRICE_SPREAD_PRECISION_U128, PRICE_TO_PEG_PRECISION_RATIO, QUOTE_PRECISION, }; use crate::math::position::_calculate_base_asset_value_and_pnl; use crate::math::quote_asset::{asset_to_reserve_amount, reserve_to_asset_amount}; use crate::math_error; use crate::state::market::{Market, OraclePriceData, AMM}; use crate::state::state::{PriceDivergenceGuardRails, ValidityGuardRails}; +use solana_program::log::sol_log_compute_units; pub fn calculate_price( quote_asset_reserve: u128, @@ -360,8 +365,7 @@ pub fn calculate_oracle_mark_spread_pct( let (oracle_price, price_spread) = calculate_oracle_mark_spread(amm, oracle_price_data, precomputed_mark_price)?; - price_spread - .checked_mul(PRICE_SPREAD_PRECISION) + multiply_i128(price_spread, PRICE_SPREAD_PRECISION) .ok_or_else(math_error!())? .checked_div(oracle_price) .ok_or_else(math_error!()) @@ -452,60 +456,178 @@ pub fn is_oracle_valid( || is_conf_too_large)) } +pub fn calculate_budgeted_k_scale( + market: &mut Market, + budget: i128, + mark_price: u128, +) -> ClearingHouseResult<(u128, u128)> { + // 0 - 100 + let curve_update_intensity = cast_to_i128(min(market.amm.curve_update_intensity, 100_u8))?; + + if curve_update_intensity == 0 { + return Ok((1, 1)); + } + + let mark_div_budget = cast_to_i128(mark_price)? + .checked_div(budget) + .ok_or_else(math_error!())?; + + let net_position = market.base_asset_amount; + let one_div_net_position = MARK_PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128 + .checked_div(net_position) + .ok_or_else(math_error!())?; + let base_asset_reserve = cast_to_i128(market.amm.base_asset_reserve)?; + + let mut numerator = mark_div_budget + .checked_add(one_div_net_position) + .ok_or_else(math_error!())? + .checked_add( + MARK_PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128 + .checked_div(base_asset_reserve) + .ok_or_else(math_error!())?, + ) + .ok_or_else(math_error!())?; + + let mut denominator = mark_div_budget + .checked_sub(one_div_net_position) + .ok_or_else(math_error!())? + .checked_sub( + multiply_i128(base_asset_reserve, one_div_net_position) + .ok_or_else(math_error!())? + .checked_div(net_position) + .ok_or_else(math_error!())?, + ) + .ok_or_else(math_error!())?; + + if numerator < 0 && denominator < 0 { + numerator = numerator.abs(); + denominator = denominator.abs(); + } + + assert!((numerator > 0 && denominator > 0)); + + if budget < 0 && numerator > denominator { + assert!(false); + } + + let (numerator, denominator) = if numerator > denominator { + let k_pct_upper_bound = K_BPS_UPDATE_SCALE + (K_BPS_INCREASE_MAX * curve_update_intensity / 100); + + let current_pct_change = multiply_i128(numerator, 1000) + .ok_or_else(math_error!())? + .checked_div(denominator) + .ok_or_else(math_error!())?; + + let maximum_pct_change = multiply_i128(k_pct_upper_bound, 1000) + .ok_or_else(math_error!())? + .checked_div(K_BPS_UPDATE_SCALE) + .ok_or_else(math_error!())?; + + if current_pct_change > maximum_pct_change { + (k_pct_upper_bound, K_BPS_UPDATE_SCALE) + } else { + (numerator, denominator) + } + } else { + let k_pct_lower_bound = K_BPS_UPDATE_SCALE - (K_BPS_DECREASE_MAX * curve_update_intensity / 100); + + let current_pct_change = multiply_i128(numerator, 1000) + .ok_or_else(math_error!())? + .checked_div(denominator) + .ok_or_else(math_error!())?; + + let maximum_pct_change = multiply_i128(k_pct_lower_bound, 1000) + .ok_or_else(math_error!())? + .checked_div(K_BPS_UPDATE_SCALE) + .ok_or_else(math_error!())?; + + if current_pct_change < maximum_pct_change { + (k_pct_lower_bound, K_BPS_UPDATE_SCALE) + } else { + (numerator, denominator) + } + }; + + Ok((cast_to_u128(numerator)?, cast_to_u128(denominator)?)) +} + /// To find the cost of adjusting k, compare the the net market value before and after adjusting k /// Increasing k costs the protocol money because it reduces slippage and improves the exit price for net market position /// Decreasing k costs the protocol money because it increases slippage and hurts the exit price for net market position -pub fn adjust_k_cost(market: &mut Market, new_sqrt_k: bn::U256) -> ClearingHouseResult { +pub fn adjust_k_cost( + market: &Market, + update_k_result: &UpdateKResult, +) -> ClearingHouseResult { + let mut market_clone = *market; + // Find the net market value before adjusting k let (current_net_market_value, _) = - _calculate_base_asset_value_and_pnl(market.base_asset_amount, 0, &market.amm)?; + _calculate_base_asset_value_and_pnl(market_clone.base_asset_amount, 0, &market_clone.amm)?; - let mark_price_precision = bn::U256::from(MARK_PRICE_PRECISION); + update_k(&mut market_clone, update_k_result)?; + let (_new_net_market_value, cost) = _calculate_base_asset_value_and_pnl( + market_clone.base_asset_amount, + current_net_market_value, + &market_clone.amm, + )?; + Ok(cost) +} + +pub struct UpdateKResult { + pub sqrt_k: u128, + pub base_asset_reserve: u128, + pub quote_asset_reserve: u128, +} + +pub fn get_update_k_result( + market: &Market, + new_sqrt_k: bn::U192, +) -> ClearingHouseResult { + let sqrt_k_ratio_precision = bn::U192::from(10_000); + + let old_sqrt_k = bn::U192::from(market.amm.sqrt_k); let sqrt_k_ratio = new_sqrt_k - .checked_mul(mark_price_precision) + .checked_mul(sqrt_k_ratio_precision) .ok_or_else(math_error!())? - .checked_div(bn::U256::from(market.amm.sqrt_k)) + .checked_div(old_sqrt_k) .ok_or_else(math_error!())?; // if decreasing k, max decrease ratio for single transaction is 2.5% - if sqrt_k_ratio - < mark_price_precision - .checked_mul(bn::U256::from(975)) - .ok_or_else(math_error!())? - .checked_div(bn::U256::from(1000)) - .ok_or_else(math_error!())? - { + if sqrt_k_ratio < U192::from(9750) { return Err(ErrorCode::InvalidUpdateK); } - market.amm.sqrt_k = new_sqrt_k.try_to_u128().unwrap(); - market.amm.base_asset_reserve = bn::U256::from(market.amm.base_asset_reserve) + let sqrt_k = new_sqrt_k.try_to_u128().unwrap(); + let base_asset_reserve = bn::U192::from(market.amm.base_asset_reserve) .checked_mul(sqrt_k_ratio) .ok_or_else(math_error!())? - .checked_div(mark_price_precision) + .checked_div(sqrt_k_ratio_precision) .ok_or_else(math_error!())? - .try_to_u128() - .unwrap(); + .try_to_u128()?; - let invariant_sqrt_u192 = U192::from(market.amm.sqrt_k); + let invariant_sqrt_u192 = U192::from(sqrt_k); let invariant = invariant_sqrt_u192 .checked_mul(invariant_sqrt_u192) .ok_or_else(math_error!())?; - market.amm.quote_asset_reserve = invariant - .checked_div(U192::from(market.amm.base_asset_reserve)) + let quote_asset_reserve = invariant + .checked_div(U192::from(base_asset_reserve)) .ok_or_else(math_error!())? - .try_to_u128() - .unwrap(); + .try_to_u128()?; - let (_new_net_market_value, cost) = _calculate_base_asset_value_and_pnl( - market.base_asset_amount, - current_net_market_value, - &market.amm, - )?; + Ok(UpdateKResult { + sqrt_k, + base_asset_reserve, + quote_asset_reserve, + }) +} - Ok(cost) +pub fn update_k(market: &mut Market, update_k_result: &UpdateKResult) -> ClearingHouseResult { + market.amm.sqrt_k = update_k_result.sqrt_k; + market.amm.base_asset_reserve = update_k_result.base_asset_reserve; + market.amm.quote_asset_reserve = update_k_result.quote_asset_reserve; + Ok(()) } pub fn calculate_max_base_asset_amount_to_trade( diff --git a/programs/clearing_house/src/math/bn.rs b/programs/clearing_house/src/math/bn.rs index 14e44366..64417418 100644 --- a/programs/clearing_house/src/math/bn.rs +++ b/programs/clearing_house/src/math/bn.rs @@ -134,3 +134,48 @@ impl U192 { impl_borsh_deserialize_for_bn!(U192); impl_borsh_serialize_for_bn!(U192); + +construct_uint! { + /// 128-bit unsigned integer. + pub struct U128(2); +} + +impl U128 { + /// Convert u192 to u64 + pub fn to_u64(self) -> Option { + self.try_to_u64().map_or_else(|_| None, Some) + } + + /// Convert u192 to u64 + pub fn try_to_u64(self) -> ClearingHouseResult { + self.try_into().map_err(|_| BnConversionError) + } + + /// Convert u192 to u128 + pub fn to_u128(self) -> Option { + self.try_to_u128().map_or_else(|_| None, Some) + } + + /// Convert u192 to u128 + pub fn try_to_u128(self) -> ClearingHouseResult { + self.try_into().map_err(|_| BnConversionError) + } + + /// Convert from little endian bytes + pub fn from_le_bytes(bytes: [u8; 24]) -> Self { + U128::from_little_endian(&bytes) + } + + /// Convert to little endian bytes + pub fn to_le_bytes(self) -> [u8; 24] { + let mut buf: Vec = Vec::with_capacity(size_of::()); + self.to_little_endian(buf.borrow_mut()); + + let mut bytes: [u8; 24] = [0u8; 24]; + bytes.copy_from_slice(buf.as_slice()); + bytes + } +} + +impl_borsh_deserialize_for_bn!(U128); +impl_borsh_serialize_for_bn!(U128); diff --git a/programs/clearing_house/src/math/bn_operations.rs b/programs/clearing_house/src/math/bn_operations.rs new file mode 100644 index 00000000..4d56569b --- /dev/null +++ b/programs/clearing_house/src/math/bn_operations.rs @@ -0,0 +1,21 @@ +use crate::error::ClearingHouseResult; +use crate::math::bn::U128; + +use crate::error::*; +use crate::math::casting::cast_to_i128; +use crate::math_error; +use num_traits::ToPrimitive; +use solana_program::msg; + +pub fn multiply_u128(a: u128, b: u128) -> Option { + U128::from(a).checked_mul(U128::from(b))?.try_to_u128().ok() +} + +pub fn multiply_i128(a: i128, b: i128) -> Option { + U128::from(a.unsigned_abs()) + .checked_mul(U128::from(b.unsigned_abs()))? + .try_to_u128() + .ok()? + .to_i128() + .map(|c| c * a.signum() * b.signum()) +} diff --git a/programs/clearing_house/src/math/constants.rs b/programs/clearing_house/src/math/constants.rs index bc19f1a8..422ba2b1 100644 --- a/programs/clearing_house/src/math/constants.rs +++ b/programs/clearing_house/src/math/constants.rs @@ -1,6 +1,7 @@ // PRECISIONS pub const AMM_RESERVE_PRECISION: u128 = 10_000_000_000_000; //expo = -13; pub const MARK_PRICE_PRECISION: u128 = 10_000_000_000; //expo = -10 +pub const MARK_PRICE_PRECISION_I128: i128 = 10_000_000_000; //expo = -10 pub const QUOTE_PRECISION: u128 = 1_000_000; // expo = -6 pub const FUNDING_PAYMENT_PRECISION: u128 = 10_000; // expo = -4 pub const MARGIN_PRECISION: u128 = 10_000; // expo = -4 @@ -16,15 +17,21 @@ pub const AMM_TO_QUOTE_PRECISION_RATIO_I128: i128 = (AMM_RESERVE_PRECISION / QUOTE_PRECISION) as i128; // expo: 7 pub const AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO: u128 = AMM_RESERVE_PRECISION * PEG_PRECISION / QUOTE_PRECISION; // expo: 10 +pub const FUNDING_RATE_PRECISION: u128 = MARK_PRICE_PRECISION * FUNDING_PAYMENT_PRECISION; // export: 14 +pub const FUNDING_RATE_PRECISION_I128: i128 = FUNDING_RATE_PRECISION as i128; // export: 14 pub const QUOTE_TO_BASE_AMT_FUNDING_PRECISION: i128 = - (AMM_RESERVE_PRECISION * MARK_PRICE_PRECISION * FUNDING_PAYMENT_PRECISION / QUOTE_PRECISION) - as i128; // expo: 21 + (AMM_RESERVE_PRECISION * FUNDING_RATE_PRECISION / QUOTE_PRECISION) as i128; // expo: 21 pub const PRICE_TO_QUOTE_PRECISION_RATIO: u128 = MARK_PRICE_PRECISION / QUOTE_PRECISION; // expo: 4 pub const MARK_PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO: u128 = MARK_PRICE_PRECISION * AMM_TO_QUOTE_PRECISION_RATIO; // expo 17 +pub const MARK_PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128: i128 = + MARK_PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO as i128; // expo 17 pub const FUNDING_EXCESS_TO_QUOTE_RATIO: i128 = - (MARK_PRICE_PRECISION * AMM_RESERVE_PRECISION / QUOTE_PRECISION) as i128; // expo 11 + (MARK_PRICE_PRECISION * AMM_RESERVE_PRECISION / QUOTE_PRECISION) as i128; // expo 17 + +pub const AMM_TIMES_PEG_PRECISION: i128 = (AMM_RESERVE_PRECISION * PEG_PRECISION) as i128; // expo 16 +pub const AMM_RESERVE_PRECISION_I128: i128 = (AMM_RESERVE_PRECISION) as i128; // FEE REBATES pub const SHARE_OF_FEES_ALLOCATED_TO_CLEARING_HOUSE_NUMERATOR: u128 = 1; @@ -61,3 +68,14 @@ pub const MAX_LIQUIDATION_SLIPPAGE_U128: u128 = 100; // expo = -2 pub const MAX_MARK_TWAP_DIVERGENCE: u128 = 5_000; // expo = -3 pub const MAXIMUM_MARGIN_RATIO: u32 = MARGIN_PRECISION as u32; pub const MINIMUM_MARGIN_RATIO: u32 = MARGIN_PRECISION as u32 / 50; + +// FORMULAIC REPEG / K +pub const K_BPS_UPDATE_SCALE: i128 = 1_000_000; // expo = -6 (represents 100%) + // hardcoded scale bounds for a single k update (.1% increase and .09% decrease). scaled by market curve_update_intensity +pub const K_BPS_DECREASE_MAX: i128 = 900; // 9 bps decrease (900/K_BPS_UPDATE_SCALE) +pub const K_BPS_INCREASE_MAX: i128 = 1000; // 10 bps increase + +pub const PEG_BPS_UPDATE_SCALE: u128 = 1_000_000; // expo = -6 (represents 100%) + // hardcoded scale bounds for a single repeg update. scaled by market curve_update_intensity +pub const PEG_BPS_DECREASE_MAX: u128 = 1000; // 10 bps decrease +pub const PEG_BPS_INCREASE_MAX: u128 = 1000; // 10 bps increase diff --git a/programs/clearing_house/src/math/funding.rs b/programs/clearing_house/src/math/funding.rs index ea09e533..3cc4e654 100644 --- a/programs/clearing_house/src/math/funding.rs +++ b/programs/clearing_house/src/math/funding.rs @@ -18,7 +18,7 @@ use std::cmp::max; pub fn calculate_funding_rate_long_short( market: &mut Market, funding_rate: i128, -) -> ClearingHouseResult<(i128, i128)> { +) -> ClearingHouseResult<(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 clearing house receives payment let net_market_position = market.base_asset_amount; @@ -38,7 +38,11 @@ pub fn calculate_funding_rate_long_short( .net_revenue_since_last_funding .checked_add(uncapped_funding_pnl as i64) .ok_or_else(math_error!())?; - return Ok((funding_rate, funding_rate)); + return Ok(( + funding_rate, + funding_rate, + net_market_position_funding_payment, + )); } let (capped_funding_rate, capped_funding_pnl) = @@ -79,7 +83,11 @@ pub fn calculate_funding_rate_long_short( funding_rate }; - Ok((funding_rate_long, funding_rate_short)) + Ok(( + funding_rate_long, + funding_rate_short, + net_market_position_funding_payment, + )) } fn calculate_capped_funding_rate( diff --git a/programs/clearing_house/src/math/mod.rs b/programs/clearing_house/src/math/mod.rs index bd7da2c0..88026d7a 100644 --- a/programs/clearing_house/src/math/mod.rs +++ b/programs/clearing_house/src/math/mod.rs @@ -1,5 +1,6 @@ pub mod amm; pub mod bn; +pub mod bn_operations; pub mod casting; pub mod collateral; pub mod constants; diff --git a/programs/clearing_house/src/math/repeg.rs b/programs/clearing_house/src/math/repeg.rs index bc9b313f..7d94e60d 100644 --- a/programs/clearing_house/src/math/repeg.rs +++ b/programs/clearing_house/src/math/repeg.rs @@ -1,16 +1,21 @@ +use crate::controller::amm::SwapDirection; use crate::error::*; use crate::math::amm; use crate::math::bn; +use crate::math::bn_operations::{multiply_i128, multiply_u128}; use crate::math::casting::{cast_to_i128, cast_to_u128}; use crate::math::constants::{ - AMM_TO_QUOTE_PRECISION_RATIO, FUNDING_EXCESS_TO_QUOTE_RATIO, MARK_PRICE_PRECISION, ONE_HOUR, - PEG_PRECISION, PRICE_SPREAD_PRECISION, PRICE_TO_PEG_PRECISION_RATIO, QUOTE_PRECISION, + AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, AMM_TO_QUOTE_PRECISION_RATIO, + AMM_TO_QUOTE_PRECISION_RATIO_I128, FUNDING_EXCESS_TO_QUOTE_RATIO, MARK_PRICE_PRECISION, + MARK_PRICE_PRECISION_I128, ONE_HOUR, PEG_BPS_DECREASE_MAX, PEG_BPS_INCREASE_MAX, + PEG_BPS_UPDATE_SCALE, PEG_PRECISION, PRICE_SPREAD_PRECISION, PRICE_SPREAD_PRECISION_U128, + PRICE_TO_PEG_PRECISION_RATIO, QUOTE_PRECISION, SHARE_OF_FEES_ALLOCATED_TO_CLEARING_HOUSE_DENOMINATOR, SHARE_OF_FEES_ALLOCATED_TO_CLEARING_HOUSE_NUMERATOR, TWENTYFOUR_HOUR, }; use crate::math::position::_calculate_base_asset_value_and_pnl; use crate::math_error; -use crate::state::market::{Market, OraclePriceData}; +use crate::state::market::{Market, OraclePriceData, AMM}; use std::cmp::{max, min}; use crate::state::state::OracleGuardRails; @@ -23,7 +28,7 @@ pub fn calculate_repeg_validity_from_oracle_account( terminal_price_before: u128, clock_slot: u64, oracle_guard_rails: &OracleGuardRails, -) -> ClearingHouseResult<(bool, bool, bool, bool, i128)> { +) -> ClearingHouseResult<(bool, bool, bool, bool)> { let oracle_price_data = market .amm .get_oracle_price(oracle_account_info, clock_slot)?; @@ -33,25 +38,19 @@ pub fn calculate_repeg_validity_from_oracle_account( &oracle_guard_rails.validity, )?; - let ( - oracle_is_valid, - direction_valid, - profitability_valid, - price_impact_valid, - oracle_terminal_divergence_pct_after, - ) = calculate_repeg_validity( - market, - &oracle_price_data, - oracle_is_valid, - terminal_price_before, - )?; + let (oracle_is_valid, direction_valid, profitability_valid, price_impact_valid) = + calculate_repeg_validity( + market, + &oracle_price_data, + oracle_is_valid, + terminal_price_before, + )?; Ok(( oracle_is_valid, direction_valid, profitability_valid, price_impact_valid, - oracle_terminal_divergence_pct_after, )) } @@ -60,7 +59,7 @@ pub fn calculate_repeg_validity( oracle_price_data: &OraclePriceData, oracle_is_valid: bool, terminal_price_before: u128, -) -> ClearingHouseResult<(bool, bool, bool, bool, i128)> { +) -> ClearingHouseResult<(bool, bool, bool, bool)> { let OraclePriceData { price: oracle_price, confidence: oracle_conf, @@ -72,14 +71,6 @@ pub fn calculate_repeg_validity( let (terminal_price_after, _terminal_quote_reserves, _terminal_base_reserves) = amm::calculate_terminal_price_and_reserves(market)?; - let oracle_terminal_spread_after = oracle_price - .checked_sub(cast_to_i128(terminal_price_after)?) - .ok_or_else(math_error!())?; - let oracle_terminal_divergence_pct_after = oracle_terminal_spread_after - .checked_mul(PRICE_SPREAD_PRECISION) - .ok_or_else(math_error!())? - .checked_div(oracle_price) - .ok_or_else(math_error!())?; let mut direction_valid = true; let mut price_impact_valid = true; @@ -156,7 +147,6 @@ pub fn calculate_repeg_validity( direction_valid, profitability_valid, price_impact_valid, - oracle_terminal_divergence_pct_after, )) } @@ -176,25 +166,88 @@ pub fn calculate_peg_from_target_price( Ok(new_peg) } +pub fn calculate_amm_target_price( + amm: &AMM, + current_price: u128, + oracle_price_data: &OraclePriceData, +) -> ClearingHouseResult { + // calculates peg_multiplier that changing to would cost no more than budget + let oracle_price_normalised = cast_to_u128(amm::normalise_oracle_price( + amm, + oracle_price_data, + Some(current_price), + )?)?; + + let weight_denom = 100_u128; + + let delay_penalty = max( + 0, + oracle_price_data + .delay + .checked_mul(max( + 1, + oracle_price_data + .delay + .checked_div(2) + .ok_or_else(math_error!())?, + )) + .ok_or_else(math_error!())?, + ); + + let oracle_price_weight: u128 = cast_to_u128(max( + 0, + 50_i64 + .checked_sub(delay_penalty) + .ok_or_else(math_error!())?, + ))?; + + let target_price = if oracle_price_weight > 0 { + let current_price_weight: u128 = weight_denom + .checked_sub(oracle_price_weight) + .ok_or_else(math_error!())?; + + oracle_price_normalised + .checked_mul(oracle_price_weight) + .ok_or_else(math_error!())? + .checked_div(weight_denom) + .ok_or_else(math_error!())? + .checked_add( + current_price + .checked_mul(current_price_weight) + .ok_or_else(math_error!())? + .checked_div(weight_denom) + .ok_or_else(math_error!())?, + ) + .ok_or_else(math_error!())? + } else { + current_price + }; + + Ok(target_price) +} + pub fn calculate_budgeted_peg( market: &mut Market, terminal_quote_reserves: u128, budget: u128, current_price: u128, - target_price: u128, + oracle_price_data: &OraclePriceData, ) -> ClearingHouseResult<(u128, i128, Market)> { - // calculates peg_multiplier that changing to would cost no more than budget - + let target_price = calculate_amm_target_price(&market.amm, current_price, oracle_price_data)?; let optimal_peg = calculate_peg_from_target_price( market.amm.quote_asset_reserve, market.amm.base_asset_reserve, - target_price - .checked_add(current_price) - .ok_or_else(math_error!())? - .checked_div(2) - .ok_or_else(math_error!())?, + target_price, )?; + // 0-100 + let curve_update_intensity = cast_to_i128(min(market.amm.curve_update_intensity, 100_u8))?; + + // return early + if optimal_peg == market.amm.peg_multiplier || curve_update_intensity == 0 { + return Ok((market.amm.peg_multiplier, 0, *market)); + } + let delta_peg_sign = if market.amm.quote_asset_reserve > terminal_quote_reserves { 1 } else { @@ -265,9 +318,37 @@ pub fn calculate_budgeted_peg( full_budget_peg }; - let (repegged_market, candidate_cost) = adjust_peg_cost(market, candidate_peg)?; + // add bounds to single update + let capped_candidate_peg = if candidate_peg > market.amm.peg_multiplier { + let peg_upper_bound = market + .amm + .peg_multiplier + .checked_add( + multiply_u128(market.amm.peg_multiplier, PEG_BPS_INCREASE_MAX) + .ok_or_else(math_error!())? + .checked_div(PEG_BPS_UPDATE_SCALE) + .ok_or_else(math_error!())?, + ) + .ok_or_else(math_error!())?; + min(candidate_peg, peg_upper_bound) + } else { + let peg_lower_bound = market + .amm + .peg_multiplier + .checked_sub( + multiply_u128(market.amm.peg_multiplier, PEG_BPS_DECREASE_MAX) + .ok_or_else(math_error!())? + .checked_div(PEG_BPS_UPDATE_SCALE) + .ok_or_else(math_error!())?, + ) + .ok_or_else(math_error!())?; + + max(candidate_peg, peg_lower_bound) + }; + + let (repegged_market, candidate_cost) = adjust_peg_cost(market, capped_candidate_peg)?; - Ok((candidate_peg, candidate_cost, repegged_market)) + Ok((capped_candidate_peg, candidate_cost, repegged_market)) } pub fn adjust_peg_cost( @@ -350,13 +431,19 @@ pub fn calculate_expected_excess_funding_payment( .ok_or_else(math_error!())?, )?; - let expected_excess_funding_payment = market + let base_asset_amount = market .base_asset_amount - .checked_mul(expected_excess_funding) - .ok_or_else(math_error!())? + .checked_div(AMM_TO_QUOTE_PRECISION_RATIO_I128) + .ok_or_else(math_error!())?; + + let adjusted_excess_funding = expected_excess_funding .checked_div(period_adjustment) .ok_or_else(math_error!())? - .checked_div(FUNDING_EXCESS_TO_QUOTE_RATIO) + .checked_div(MARK_PRICE_PRECISION_I128) + .ok_or_else(math_error!())?; + + let expected_excess_funding_payment = base_asset_amount + .checked_mul(adjusted_excess_funding) .ok_or_else(math_error!())?; Ok(expected_excess_funding_payment) diff --git a/programs/clearing_house/src/optional_accounts.rs b/programs/clearing_house/src/optional_accounts.rs index 5204f242..ab4db732 100644 --- a/programs/clearing_house/src/optional_accounts.rs +++ b/programs/clearing_house/src/optional_accounts.rs @@ -1,6 +1,7 @@ use crate::context::{InitializeUserOptionalAccounts, ManagePositionOptionalAccounts, OrderParams}; use crate::error::{ClearingHouseResult, ErrorCode}; use crate::print_error; +use crate::state::history::curve::ExtendedCurveHistory; use crate::state::market::Markets; use crate::state::user::User; use crate::state::user_orders::UserOrders; @@ -256,3 +257,19 @@ pub fn get_oracle_for_cancel_order_by_user_order_id<'a, 'b, 'c, 'd>( Ok(oracle) } + +pub fn get_extended_curve_history<'a, 'b, 'c>( + accounts: &'a [AccountInfo<'b>], + extended_curve_history_key: &'c Pubkey, +) -> ClearingHouseResult>> { + let account_info_iter = &mut accounts.iter(); + let account_info = + account_info_iter.find(|account_info| account_info.key.eq(extended_curve_history_key)); + + match account_info { + None => Ok(None), + Some(account_info) => AccountLoader::try_from(account_info) + .map(Some) + .or(Err(ErrorCode::CouldNotDeserializeCurveHistory)), + } +} diff --git a/programs/clearing_house/src/state/market.rs b/programs/clearing_house/src/state/market.rs index 65a302e3..f3e3abd6 100644 --- a/programs/clearing_house/src/state/market.rs +++ b/programs/clearing_house/src/state/market.rs @@ -100,8 +100,13 @@ pub struct AMM { // upgrade-ability pub net_revenue_since_last_funding: i64, - pub padding2: u128, - pub padding3: u128, + pub curve_update_intensity: u8, + + pub padding2: u8, + pub padding3: u16, + pub padding4: u32, + pub padding5: u64, + pub padding6: u128, } impl AMM { diff --git a/programs/pyth/src/lib.rs b/programs/pyth/src/lib.rs index e0f7ac69..ef92a8f1 100644 --- a/programs/pyth/src/lib.rs +++ b/programs/pyth/src/lib.rs @@ -23,6 +23,7 @@ pub mod pyth { price_oracle.agg.price = price; price_oracle.agg.conf = 0; + price_oracle.valid_slot = 228506959; //todo just turned 1->2 for negative delay price_oracle.twap = price; price_oracle.expo = expo; diff --git a/sdk/src/admin.ts b/sdk/src/admin.ts index 2178a508..a6345d33 100644 --- a/sdk/src/admin.ts +++ b/sdk/src/admin.ts @@ -28,6 +28,7 @@ import { getAdmin, getWebSocketClearingHouseConfig, } from './factory/clearingHouse'; +import { assert } from './assert/assert'; export class Admin extends ClearingHouse { public static from( @@ -467,6 +468,26 @@ export class Admin extends ClearingHouse { }); } + public async updateCurveUpdateIntensity( + marketIndex: BN, + curveUpdateIntensity: number + ): Promise { + assert(curveUpdateIntensity >= 0 && curveUpdateIntensity <= 100); + assert(Number.isInteger(curveUpdateIntensity)); + + return await this.program.rpc.updateCurveUpdateIntensity( + marketIndex, + curveUpdateIntensity, + { + accounts: { + admin: this.wallet.publicKey, + state: await this.getStatePublicKey(), + markets: this.getStateAccount().markets, + }, + } + ); + } + public async updateMarginRatio( marketIndex: BN, marginRatioInitial: number, diff --git a/sdk/src/clearingHouse.ts b/sdk/src/clearingHouse.ts index 0cb02c4e..ad326b3c 100644 --- a/sdk/src/clearingHouse.ts +++ b/sdk/src/clearingHouse.ts @@ -659,10 +659,16 @@ export class ClearingHouse { }); } + const state = this.getStateAccount(); + remainingAccounts.push({ + pubkey: state.extendedCurveHistory, + isWritable: true, + isSigner: false, + }); + const priceOracle = this.getMarketsAccount().markets[marketIndex.toNumber()].amm.oracle; - const state = this.getStateAccount(); return await this.program.instruction.openPosition( direction, amount, @@ -1186,6 +1192,12 @@ export class ClearingHouse { } const state = this.getStateAccount(); + remainingAccounts.push({ + pubkey: state.extendedCurveHistory, + isWritable: true, + isSigner: false, + }); + return await this.program.instruction.closePosition( marketIndex, optionalAccounts, @@ -1311,6 +1323,7 @@ export class ClearingHouse { markets: state.markets, oracle: oracle, fundingRateHistory: state.fundingRateHistory, + extendedCurveHistory: state.extendedCurveHistory, }, }); } diff --git a/sdk/src/idl/clearing_house.json b/sdk/src/idl/clearing_house.json index 7bd288a1..dbae17fc 100644 --- a/sdk/src/idl/clearing_house.json +++ b/sdk/src/idl/clearing_house.json @@ -1561,6 +1561,11 @@ "name": "fundingRateHistory", "isMut": true, "isSigner": false + }, + { + "name": "extendedCurveHistory", + "isMut": true, + "isSigner": false } ], "args": [ @@ -1636,6 +1641,36 @@ ], "args": [] }, + { + "name": "updateCurveUpdateIntensity", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "markets", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u64" + }, + { + "name": "curveUpdateIntensity", + "type": "u8" + } + ] + }, { "name": "updateMarginRatio", "accounts": [ @@ -3295,12 +3330,28 @@ "name": "netRevenueSinceLastFunding", "type": "i64" }, + { + "name": "curveUpdateIntensity", + "type": "u8" + }, { "name": "padding2", - "type": "u128" + "type": "u8" }, { "name": "padding3", + "type": "u16" + }, + { + "name": "padding4", + "type": "u32" + }, + { + "name": "padding5", + "type": "u64" + }, + { + "name": "padding6", "type": "u128" } ] @@ -4305,6 +4356,11 @@ "code": 6061, "name": "InvalidRepegPriceImpact", "msg": "AMM repeg mark price impact vs oracle too large" + }, + { + "code": 6062, + "name": "CouldNotDeserializeCurveHistory", + "msg": "Could not deserialize curve history" } ] } \ No newline at end of file diff --git a/sdk/src/math/repeg.ts b/sdk/src/math/repeg.ts index 104261de..9f3c6b6d 100644 --- a/sdk/src/math/repeg.ts +++ b/sdk/src/math/repeg.ts @@ -185,6 +185,14 @@ export function calculateBudgetedK(market: Market, cost: BN): [BN, BN] { const C = cost.mul(new BN(-1)); + console.log( + convertToNumber(x, AMM_RESERVE_PRECISION), + convertToNumber(y, AMM_RESERVE_PRECISION), + convertToNumber(d, AMM_RESERVE_PRECISION), + Q.toNumber() / 1e3, + C.toNumber() / 1e6 + ); + const numer1 = y.mul(d).mul(Q).div(AMM_RESERVE_PRECISION).div(PEG_PRECISION); const numer2 = C.mul(x.add(d)).div(QUOTE_PRECISION); const denom1 = C.mul(x) @@ -199,8 +207,11 @@ export function calculateBudgetedK(market: Market, cost: BN): [BN, BN] { .div(AMM_RESERVE_PRECISION) .div(PEG_PRECISION); + console.log('numers:', convertToNumber(numer1), convertToNumber(numer2)); + console.log('denoms:', convertToNumber(denom1), convertToNumber(denom2)); + const numerator = d - .mul(numer1.add(numer2)) + .mul(numer1.sub(numer2)) .div(AMM_RESERVE_PRECISION) .div(AMM_RESERVE_PRECISION) .div(AMM_TO_QUOTE_PRECISION_RATIO); @@ -211,8 +222,12 @@ export function calculateBudgetedK(market: Market, cost: BN): [BN, BN] { console.log(numerator, denominator); // const p = (numerator).div(denominator); - // const formulaCost = (numer21.sub(numer20).sub(numer1)).mul(market.amm.pegMultiplier).div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO) - // console.log(convertToNumber(formulaCost, QUOTE_PRECISION)) + // const formulaCost = numer21 + // .sub(numer20) + // .sub(numer1) + // .mul(market.amm.pegMultiplier) + // .div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO); + // console.log(convertToNumber(formulaCost, QUOTE_PRECISION)); return [numerator, denominator]; } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 6886e606..64e1a293 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -122,6 +122,7 @@ export type DepositRecord = { export type ExtendedCurveRecord = { ts: BN; + adjustmentCost: BN; recordId: BN; marketIndex: BN; pegMultiplierBefore: BN; diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index d5a9cffc..f72bae5d 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -4,7 +4,7 @@ if [ "$1" != "--skip-build" ] cp target/idl/clearing_house.json sdk/src/idl/ fi -test_files=(formulaPeg.ts order.ts orderReferrer.ts marketOrder.ts triggerOrders.ts stopLimits.ts userOrderId.ts makerOrder.ts roundInFavorBaseAsset.ts marketOrderBaseAssetAmount.ts expireOrders.ts oracleOffsetOrders.ts cancelAllOrders.ts roundReduceOnlyOrder.ts clearingHouse.ts pyth.ts userAccount.ts admin.ts updateK.ts adminWithdraw.ts curve.ts whitelist.ts fees.ts idempotentCurve.ts maxDeposit.ts deleteUser.ts maxPositions.ts maxReserves.ts twapDivergenceLiquidation.ts oraclePnlLiquidation.ts whaleLiquidation.ts roundInFavor.ts minimumTradeSize.ts cappedSymFunding.ts) +test_files=(formulaK.ts formulaPeg.ts order.ts orderReferrer.ts marketOrder.ts triggerOrders.ts stopLimits.ts userOrderId.ts makerOrder.ts roundInFavorBaseAsset.ts marketOrderBaseAssetAmount.ts expireOrders.ts oracleOffsetOrders.ts clearingHouse.ts pyth.ts userAccount.ts admin.ts updateK.ts adminWithdraw.ts curve.ts whitelist.ts fees.ts idempotentCurve.ts maxDeposit.ts maxPositions.ts maxReserves.ts twapDivergenceLiquidation.ts oraclePnlLiquidation.ts whaleLiquidation.ts roundInFavor.ts minimumTradeSize.ts cappedSymFunding.ts) for test_file in ${test_files[@]}; do export ANCHOR_TEST_FILE=${test_file} && anchor test --skip-build || exit 1; diff --git a/tests/curve.ts b/tests/curve.ts index 34cc17a9..d536d688 100644 --- a/tests/curve.ts +++ b/tests/curve.ts @@ -61,7 +61,7 @@ describe('AMM Curve', () => { userUSDCAccount = await mockUserUSDCAccount(usdcMint, usdcAmount, provider); await clearingHouse.initialize(usdcMint.publicKey, true); - await clearingHouse.subscribe(); + await clearingHouse.subscribeToAll(); solUsdOracle = await createPriceFeed({ oracleProgram: anchor.workspace.Pyth, diff --git a/tests/formulaK.ts b/tests/formulaK.ts new file mode 100644 index 00000000..00603a0e --- /dev/null +++ b/tests/formulaK.ts @@ -0,0 +1,478 @@ +import * as anchor from '@project-serum/anchor'; +import { assert } from 'chai'; +import { BN, calculateBudgetedK } from '../sdk'; + +import { Keypair } from '@solana/web3.js'; +import { Program } from '@project-serum/anchor'; +import { + Admin, + MARK_PRICE_PRECISION, + calculateMarkPrice, + ClearingHouseUser, + PEG_PRECISION, + PositionDirection, + convertToNumber, + calculateAdjustKCost, + findComputeUnitConsumption, +} from '../sdk/src'; + +import { Markets } from '../sdk/src/constants/markets'; + +import { + createPriceFeed, + mockUSDCMint, + mockUserUSDCAccount, + setFeedPrice, + getFeedData, +} from './testHelpers'; +import { QUOTE_PRECISION } from '../sdk/lib'; + +const ZERO = new BN(0); + +describe('formulaic curve (k)', () => { + const provider = anchor.Provider.local(); + const connection = provider.connection; + anchor.setProvider(provider); + const chProgram = anchor.workspace.ClearingHouse as Program; + + let clearingHouse: Admin; + + let usdcMint: Keypair; + let userUSDCAccount: Keypair; + const initialSOLPrice = 150; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(Math.sqrt(MARK_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(1e9 * 10 ** 6); + + let userAccount: ClearingHouseUser; + let solUsdOracle; + + before(async () => { + usdcMint = await mockUSDCMint(provider); + userUSDCAccount = await mockUserUSDCAccount(usdcMint, usdcAmount, provider); + + clearingHouse = Admin.from( + connection, + provider.wallet, + chProgram.programId, + { + commitment: 'confirmed', + preflightCommitment: 'confirmed', + } + ); + await clearingHouse.initialize(usdcMint.publicKey, true); + await clearingHouse.subscribeToAll(); + + const periodicity = new BN(0); // 1 HOUR + + solUsdOracle = await createPriceFeed({ + oracleProgram: anchor.workspace.Pyth, + initPrice: initialSOLPrice, + }); + + await clearingHouse.initializeMarket( + Markets[0].marketIndex, + solUsdOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(initialSOLPrice * PEG_PRECISION.toNumber()) + ); + + await clearingHouse.updateCurveUpdateIntensity(Markets[0].marketIndex, 100); + + await clearingHouse.initializeUserAccount(); + userAccount = ClearingHouseUser.from( + clearingHouse, + provider.wallet.publicKey + ); + await userAccount.subscribe(); + }); + + after(async () => { + await clearingHouse.unsubscribe(); + await userAccount.unsubscribe(); + }); + + it('track netRevenueSinceLastFunding', async () => { + await clearingHouse.depositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + const marketIndex = Markets[0].marketIndex; + + const targetPriceBack = new BN( + initialSOLPrice * MARK_PRICE_PRECISION.toNumber() + ); + + // const [direction, tradeSize, _] = clearingHouse.calculateTargetPriceTrade( + // marketIndex, + // targetPriceUp + // ); + await clearingHouse.moveAmmToPrice(marketIndex, targetPriceBack); + await clearingHouse.updateFundingPaused(true); + + console.log('taking position'); + await clearingHouse.openPosition( + PositionDirection.LONG, + new BN(1000).mul(QUOTE_PRECISION), + marketIndex + ); + console.log('$1000 position taken'); + await clearingHouse.fetchAccounts(); + const marketsOld = await clearingHouse.getMarketsAccount(); + assert(!marketsOld.markets[0].baseAssetAmount.eq(ZERO)); + + const oldKPrice = calculateMarkPrice(clearingHouse.getMarket(marketIndex)); + const ammOld = marketsOld.markets[0].amm; + console.log( + 'USER getTotalCollateral', + convertToNumber(userAccount.getTotalCollateral(), QUOTE_PRECISION) + ); + + await clearingHouse.fetchAccounts(); + const marketsKChange = await clearingHouse.getMarketsAccount(); + const ammKChange = marketsKChange.markets[0].amm; + + const newKPrice = calculateMarkPrice(clearingHouse.getMarket(marketIndex)); + + console.log('$1000 position closing'); + + await clearingHouse.closePosition(marketIndex); + console.log('$1000 position closed'); + + const markets = await clearingHouse.getMarketsAccount(); + + const amm = markets.markets[0].amm; + + const marginOfError = new BN(MARK_PRICE_PRECISION.div(new BN(1000))); // price change less than 3 decimal places + + console.log( + 'oldSqrtK', + convertToNumber(ammOld.sqrtK), + 'oldKPrice:', + convertToNumber(oldKPrice) + ); + + console.log( + 'newSqrtK', + convertToNumber(amm.sqrtK), + 'newKPrice:', + convertToNumber(newKPrice) + ); + + assert(ammOld.sqrtK.eq(amm.sqrtK)); + assert(newKPrice.sub(oldKPrice).abs().lt(marginOfError)); + // assert(!amm.sqrtK.eq(newSqrtK)); + + console.log( + 'realizedFeeOld', + convertToNumber(ammOld.totalFeeMinusDistributions, QUOTE_PRECISION), + 'realizedFeePostK', + convertToNumber(ammKChange.totalFeeMinusDistributions, QUOTE_PRECISION), + 'realizedFeePostClose', + convertToNumber(amm.totalFeeMinusDistributions, QUOTE_PRECISION), + 'netRevenue', + convertToNumber(amm.netRevenueSinceLastFunding, QUOTE_PRECISION) + ); + + assert(amm.netRevenueSinceLastFunding.eq(amm.totalFeeMinusDistributions)); + console.log( + 'USER getTotalCollateral', + convertToNumber(userAccount.getTotalCollateral(), QUOTE_PRECISION) + ); + }); + it('update funding (no k change)', async () => { + const marketIndex = Markets[0].marketIndex; + const marketsOld = await clearingHouse.getMarketsAccount(); + const marketOld = marketsOld.markets[0]; + const ammOld = marketOld.amm; + + await clearingHouse.updateFundingPaused(false); + await new Promise((r) => setTimeout(r, 1000)); // wait 1 second + + const _tx = await clearingHouse.updateFundingRate( + solUsdOracle, + marketIndex + ); + await new Promise((r) => setTimeout(r, 1000)); // wait 1 second + + const markets = await clearingHouse.getMarketsAccount(); + const market = markets.markets[0]; + const amm = market.amm; + + console.log( + 'oldSqrtK', + convertToNumber(ammOld.sqrtK), + 'newSqrtK', + convertToNumber(amm.sqrtK) + ); + + // await setFeedPrice(program, newPrice, priceFeedAddress); + const oraclePx = await getFeedData(anchor.workspace.Pyth, amm.oracle); + + console.log( + 'markPrice:', + convertToNumber(calculateMarkPrice(market)), + 'oraclePrice:', + oraclePx.price + ); + console.log( + 'USER getTotalCollateral', + convertToNumber(userAccount.getTotalCollateral(), QUOTE_PRECISION), + 'fundingPnL:', + convertToNumber(userAccount.getUnrealizedFundingPNL(), QUOTE_PRECISION) + ); + console.log( + 'fundingRate:', + convertToNumber(amm.lastFundingRate, MARK_PRICE_PRECISION) + ); + console.log( + 'realizedFeePostClose', + convertToNumber(amm.totalFeeMinusDistributions, QUOTE_PRECISION), + 'netRevenue', + convertToNumber(amm.netRevenueSinceLastFunding, QUOTE_PRECISION) + ); + + assert(amm.netRevenueSinceLastFunding.eq(ZERO)); + assert(amm.sqrtK); + }); + + it('update funding (k increase by max .01%)', async () => { + const marketIndex = Markets[0].marketIndex; + const marketsOld = await clearingHouse.getMarketsAccount(); + const marketOld = marketsOld.markets[0]; + const ammOld = marketsOld.markets[0].amm; + assert(marketOld.baseAssetAmount.eq(ZERO)); + + console.log('taking position'); + await clearingHouse.openPosition( + PositionDirection.LONG, + new BN(10000).mul(QUOTE_PRECISION), + marketIndex + ); + console.log('$10000 position taken'); + const marketsAfterPos = await clearingHouse.getMarketsAccount(); + const marketAfterPos = marketsAfterPos.markets[0]; + // const ammAfterPos = marketAfterPos.amm; + const maxAdjCost = calculateAdjustKCost( + marketAfterPos, + marketIndex, + new BN(10010), + new BN(10000) + ); + const [pNumer, pDenom] = calculateBudgetedK(marketAfterPos, maxAdjCost); + + console.log( + 'max increase k cost:', + convertToNumber(maxAdjCost, QUOTE_PRECISION), + 'budget k back out scale: multiply by', + convertToNumber(pNumer) / convertToNumber(pDenom) + ); + + await clearingHouse.updateFundingPaused(false); + await new Promise((r) => setTimeout(r, 1000)); // wait 1 second + + const txSig = await clearingHouse.updateFundingRate( + solUsdOracle, + marketIndex + ); + const computeUnits = await findComputeUnitConsumption( + clearingHouse.program.programId, + connection, + txSig, + 'confirmed' + ); + console.log('compute units', computeUnits); + console.log( + 'tx logs', + (await connection.getTransaction(txSig, { commitment: 'confirmed' })).meta + .logMessages + ); + + await new Promise((r) => setTimeout(r, 1000)); // wait 1 second + await clearingHouse.updateFundingPaused(true); + + const markets = await clearingHouse.getMarketsAccount(); + const market = markets.markets[0]; + const amm = market.amm; + + // await setFeedPrice(program, newPrice, priceFeedAddress); + const oraclePx = await getFeedData(anchor.workspace.Pyth, amm.oracle); + + console.log( + 'markPrice:', + convertToNumber(calculateMarkPrice(market)), + 'oraclePrice:', + oraclePx.price + ); + + console.log( + 'USER getTotalCollateral', + convertToNumber(userAccount.getTotalCollateral(), QUOTE_PRECISION), + 'fundingPnL:', + convertToNumber(userAccount.getUnrealizedFundingPNL(), QUOTE_PRECISION) + ); + console.log( + 'fundingRate:', + convertToNumber(amm.lastFundingRate, MARK_PRICE_PRECISION) + ); + console.log( + 'realizedFeePostClose', + convertToNumber(amm.totalFeeMinusDistributions, QUOTE_PRECISION), + 'netRevenue', + convertToNumber(amm.netRevenueSinceLastFunding, QUOTE_PRECISION) + ); + + console.log( + 'oldSqrtK', + convertToNumber(ammOld.sqrtK), + 'newSqrtK', + convertToNumber(amm.sqrtK) + ); + + // traders over traded => increase of k + assert(amm.sqrtK.gt(ammOld.sqrtK)); + + const curveHistoryAccount = clearingHouse.getCurveHistoryAccount(); + const curveHistoryHead = curveHistoryAccount.head.toNumber(); + assert.ok(curveHistoryHead === 2); + const cRecord = curveHistoryAccount.curveRecords[curveHistoryHead - 1]; + + console.log( + 'curve cost:', + convertToNumber(cRecord.adjustmentCost, QUOTE_PRECISION) + ); + + assert(amm.netRevenueSinceLastFunding.eq(ZERO)); + }); + it('update funding (k decrease by max .009%)', async () => { + const marketIndex = Markets[0].marketIndex; + const marketsOld = await clearingHouse.getMarketsAccount(); + const marketOld = marketsOld.markets[marketIndex.toNumber()]; + const ammOld = marketOld.amm; + await setFeedPrice( + anchor.workspace.Pyth, + initialSOLPrice * 1.05, + solUsdOracle + ); + + // await setFeedPrice(program, newPrice, priceFeedAddress); + const oraclePxOld = await getFeedData(anchor.workspace.Pyth, ammOld.oracle); + + console.log( + 'markPrice:', + convertToNumber(calculateMarkPrice(marketOld)), + 'oraclePrice:', + oraclePxOld.price + ); + + const maxAdjCost = calculateAdjustKCost( + marketsOld.markets[marketIndex.toNumber()], + marketIndex, + new BN(9991), + new BN(10000) + ); + + const maxAdjCostShrink100x = calculateAdjustKCost( + marketsOld.markets[marketIndex.toNumber()], + marketIndex, + new BN(1), + new BN(100) + ); + + const [pNumer, pDenom] = calculateBudgetedK(marketOld, maxAdjCost); + + const [pNumer2, pDenom2] = calculateBudgetedK(marketOld, new BN(-112934)); // ~$.11 + + console.log( + 'max decrease k cost:', + convertToNumber(maxAdjCost, QUOTE_PRECISION), + 'budget k back out scale: multiply by', + convertToNumber(pNumer) / convertToNumber(pDenom), + '\n', + '1/100th k cost:', + convertToNumber(maxAdjCostShrink100x, QUOTE_PRECISION), + 'budget k $-13:', + convertToNumber(pNumer2) / convertToNumber(pDenom2) + ); + + // console.log('taking position'); + // await clearingHouse.openPosition( + // PositionDirection.LONG, + // new BN(10000).mul(QUOTE_PRECISION), + // marketIndex + // ); + // console.log('$10000 position taken'); + + await clearingHouse.updateFundingPaused(false); + await new Promise((r) => setTimeout(r, 1000)); // wait 1 secon + + const _tx = await clearingHouse.updateFundingRate( + solUsdOracle, + marketIndex + ); + await new Promise((r) => setTimeout(r, 1000)); // wait 1 second + await clearingHouse.updateFundingPaused(true); + + const markets = await clearingHouse.getMarketsAccount(); + const market = markets.markets[0]; + const amm = market.amm; + + // await setFeedPrice(program, newPrice, priceFeedAddress); + const oraclePx = await getFeedData(anchor.workspace.Pyth, amm.oracle); + + console.log( + 'markPrice:', + convertToNumber(calculateMarkPrice(market)), + 'oraclePrice:', + oraclePx.price + ); + + console.log( + 'USER getTotalCollateral', + convertToNumber(userAccount.getTotalCollateral(), QUOTE_PRECISION), + 'fundingPnL:', + convertToNumber(userAccount.getUnrealizedFundingPNL(), QUOTE_PRECISION) + ); + console.log( + 'fundingRate:', + convertToNumber(amm.lastFundingRate, MARK_PRICE_PRECISION) + ); + console.log( + 'realizedFeePostClose', + convertToNumber(amm.totalFeeMinusDistributions, QUOTE_PRECISION), + 'netRevenue', + convertToNumber(amm.netRevenueSinceLastFunding, QUOTE_PRECISION) + ); + + console.log( + 'oldSqrtK', + convertToNumber(ammOld.sqrtK), + 'newSqrtK', + convertToNumber(amm.sqrtK) + ); + + // traders over traded => increase of k + assert(amm.sqrtK.lt(ammOld.sqrtK)); + + const curveHistoryAccount = clearingHouse.getCurveHistoryAccount(); + const curveHistoryHead = curveHistoryAccount.head.toNumber(); + assert.ok(curveHistoryHead === 3); + const cRecord = curveHistoryAccount.curveRecords[curveHistoryHead - 1]; + + console.log( + 'curve cost:', + convertToNumber(cRecord.adjustmentCost, QUOTE_PRECISION) + ); + + assert(amm.netRevenueSinceLastFunding.eq(ZERO)); + }); +}); diff --git a/tests/formulaPeg.ts b/tests/formulaPeg.ts index f58e19fc..971da4e5 100644 --- a/tests/formulaPeg.ts +++ b/tests/formulaPeg.ts @@ -73,6 +73,18 @@ async function formRepegHelper( orderParams // discountTokenAccount.address ); + const computeUnits = await findComputeUnitConsumption( + clearingHouse.program.programId, + connection, + txSig, + 'confirmed' + ); + console.log('compute units', computeUnits); + console.log( + 'tx logs', + (await connection.getTransaction(txSig, { commitment: 'confirmed' })).meta + .logMessages + ); await clearingHouse.fetchAccounts(); await userAccount.fetchAccounts(); @@ -185,7 +197,11 @@ describe('formulaic curve (repeg)', () => { clearingHouse = Admin.from( connection, provider.wallet, - chProgram.programId + chProgram.programId, + { + commitment: 'confirmed', + preflightCommitment: 'confirmed', + } ); await clearingHouse.initialize(usdcMint.publicKey, true); await clearingHouse.subscribe(); @@ -210,6 +226,7 @@ describe('formulaic curve (repeg)', () => { periodicity, new BN(initialSOLPrice * PEG_PRECISION.toNumber()) ); + await clearingHouse.updateCurveUpdateIntensity(marketIndex, 100); await clearingHouse.initializeMarket( marketIndex.add(new BN(1)), @@ -220,6 +237,11 @@ describe('formulaic curve (repeg)', () => { new BN(110) ); + await clearingHouse.updateCurveUpdateIntensity( + marketIndex.add(new BN(1)), + 100 + ); + await clearingHouse.initializeUserAccount(); userAccount = ClearingHouseUser.from( clearingHouse, @@ -389,6 +411,7 @@ describe('formulaic curve (repeg)', () => { // net revenue above fee collected const feeCollected = newOracle * base * 0.001; + console.log('feeCollected (', feeCollected, ') < profit (', profit, ')'); assert(profit > feeCollected); const netRev2 = await formRepegHelper( @@ -429,7 +452,7 @@ describe('formulaic curve (repeg)', () => { const newOracle = 0.16; const base = 2; - await formRepegHelper( + const profit = await formRepegHelper( connection, clearingHouse, userAccount, @@ -438,6 +461,8 @@ describe('formulaic curve (repeg)', () => { base, PositionDirection.LONG ); + + console.log('profit:', profit); }); it('cause repeg? tiny prices', async () => { @@ -460,7 +485,7 @@ describe('formulaic curve (repeg)', () => { const newOracle = 0.1699; const base = 100; - await formRepegHelper( + const profit = await formRepegHelper( connection, clearingHouse, userAccount, @@ -469,5 +494,6 @@ describe('formulaic curve (repeg)', () => { base, PositionDirection.LONG ); + console.log('profit:', profit); }); });