From ce910cf8c7f473bf1f4733e495515e4875f1043b Mon Sep 17 00:00:00 2001 From: didobagi <77699357+didobagi@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:31:41 -0100 Subject: [PATCH] implement parallel processing in sync() method (#843) * sdk: release v2.54.0-beta.5 * sdk: tweak polling tx confirmation (#812) * sdk: fix tests not building * sdk: tweak polling tx confirmation * sdk: release v2.54.0-beta.6 * sdk: add option to get signed settlepnl tix from a market order (#813) * sdk: add option to get signed settlepnl tix from a market order * fix lint * sdk: release v2.54.0-beta.7 * ts-sdk: remove unncessary logs on tx failure * linter * prettify * sdk: release v2.54.0-beta.8 * math/constants.rs: correct QUOTE AMOUNTS constant name * ts-sdk: filter out erroneous logs in EventSubscriber (#815) * sdk: release v2.54.0-beta.9 * program: auto derisk lp positions in settle pnl (#766) * bigz/add-lp-order-risk-mitigations * friendly perp position iter * update parmams mutability * add time_since_last_liquidity_change * add new variable to margin context mode * add attempt to burn shares in force cancel/settle pnl * remove from force cancel orders * sdk: fix tests not building * change place orders * rmeove unused mut * address some comments * add to settle_pnl * use oracle orders * tweak order params * oracle order tests * add typescript test, fix order param market_type * remove logic from settle_expired_position * CHANGELOG --------- Co-authored-by: Chris Heaney * sdk: release v2.54.0-beta.10 * v2.54.0 * sdk: release v2.55.0-beta.0 * Revert "sdk: release v2.55.0-beta.0" This reverts commit 21c4a465b43a8fad5c3eb681a428e6022ea0e109. * Revert "v2.54.0" This reverts commit 63c72082704ea203966f26803e6a50dd2fa61dd6. * sdk: release v2.54.0-beta.11 * ts-sdk: fix mixing stopWaiting * sdk: release v2.54.0-beta.12 * v2.54.0 * sdk: release v2.55.0-beta.0 * Added handling for signature time measurement (#818) * Added handling for signature time measurement * Added opt-in metrics flag to drift client * fixed prettier --------- Co-authored-by: lowkeynicc * sdk: release v2.55.0-beta.1 * sdk: fix ts timeout * sdk: fix getUnrealizedPNL for market index 0 * sdk: release v2.55.0-beta.2 * sdk: release v2.55.0-beta.3 * sdk: dlp updates (#822) * dlp updates * add optional custom margin ratio param to initAndDeposit * prettify * sdk: release v2.55.0-beta.4 * program: fix risk reduction dlp burn from not being step size (#826) * program: fix risk reduction dlp burn from not being step size * CHANGELOG * fix lp burn * v2.55.0 * sdk: release v2.56.0-beta.0 * Luke/extra tx sending (#827) * added raw tx endpoints to tx senders * Swapped additional tx endpoints for callbacks * sdk: account for settle lp in accoutnt breakdown (#828) * sdk: add settle pnl option to cost basis calc * sdk: catch error in priorityFeeSubscriber.load (#834) * Replace sendRawTransaction with sendTransaction (#833) * program: make it easier for jit fills (#835) * program: make it easier for jit fills * CHANGELOG * sdk: prettify:fix * sdk: release v2.56.0-beta.1 * v2.56.0 * sdk: release v2.57.0-beta.0 * devnet-market-index-23 (#837) * devnet-market-index-23 * comment out used oracle * sdk: release v2.57.0-beta.1 * program: add recenter amm ix (#836) * bigz/add-amm-recenter-fcn * add recenter-amm-2 test * passing recenter_amm_2 test * add test_move_amm test * update todo msg * add to adminClient * add get_lower_bound_sqrt_k functor * CHANGELOG --------- Co-authored-by: Chris Heaney * sdk: release v2.57.0-beta.2 * v2.57.0 * sdk: release v2.58.0-beta.0 * add helius method to PriorityFeeSubscriber (#832) * add helius method to PriorityFeeSubscriber * eagerly convert PubicKey to string * use helius from connection obj, or optional rpc url * lints * sdk: release v2.58.0-beta.1 * program: AmmPaused doesnt block all fills * v2.58.0 * sdk: release v2.59.0-beta.0 * sdk: allow hotswap addresses in PriorityFeeSubscriber * sdk: release v2.59.0-beta.1 * sdk: fix priorityFeeSubscriber rpc endpoint loading * sdk: release v2.59.0-beta.2 * sdk: add max fee clamp to priority fee * sdk: release v2.59.0-beta.3 * sdk: fix priority fee clamp, add getter * sdk: release v2.59.0-beta.4 * program: bump phoenix dependency * no state ws resub * sdk: release v2.59.0-beta.5 * program: use decayed last_oracle_conf_pct as lower bound for update (#840) * bigz/add-oracle-volatility-protections * fix pct vs price bug, add test + changelog * cleanup code (naming, mutables) * program: add paused operations flag (#839) * program: init pause_operations flag * tweaks * write tests for paused operations * split out spot and perp operations * fixes * add sdk and use PerpOperation/SpotOperation naming convention * fix update funding ix * stop update to deprecated market status * add admin fn * add logging for admin fn * CHANGELOG * sdk: release v2.59.0-beta.6 * v2.59.0 * sdk: release v2.60.0-beta.0 * implement parallel processing in sync() method --------- Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lil perp Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: wphan Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> Co-authored-by: Luke Co-authored-by: lowkeynicc Co-authored-by: Chester Sim Co-authored-by: Evan Pipta <3pipta@gmail.com> Co-authored-by: Nour Alharithi --- CHANGELOG.md | 64 ++- Cargo.lock | 4 +- programs/drift/Cargo.toml | 4 +- programs/drift/src/controller/amm.rs | 53 ++ programs/drift/src/controller/lp.rs | 11 +- programs/drift/src/controller/lp/tests.rs | 41 +- programs/drift/src/controller/orders.rs | 179 ++++++- programs/drift/src/controller/pnl.rs | 73 ++- .../drift/src/controller/pnl/delisting.rs | 69 ++- programs/drift/src/controller/pnl/tests.rs | 157 ++++-- .../drift/src/controller/position/tests.rs | 319 +++++++++++- programs/drift/src/controller/spot_balance.rs | 4 +- .../drift/src/controller/spot_position.rs | 15 +- programs/drift/src/instructions/admin.rs | 56 +- programs/drift/src/instructions/keeper.rs | 18 +- programs/drift/src/instructions/user.rs | 102 ++-- programs/drift/src/lib.rs | 22 + programs/drift/src/math/amm.rs | 11 +- programs/drift/src/math/amm/tests.rs | 61 +++ programs/drift/src/math/amm_spread.rs | 7 +- programs/drift/src/math/constants.rs | 2 +- programs/drift/src/math/margin.rs | 42 +- programs/drift/src/math/margin/tests.rs | 18 +- programs/drift/src/math/matching.rs | 2 +- programs/drift/src/math/oracle.rs | 5 +- programs/drift/src/math/repeg.rs | 5 +- .../drift/src/state/margin_calculation.rs | 50 +- programs/drift/src/state/mod.rs | 1 + programs/drift/src/state/order_params.rs | 60 +++ .../drift/src/state/order_params/tests.rs | 323 ++++++++++++ programs/drift/src/state/paused_operations.rs | 62 +++ .../src/state/paused_operations/tests.rs | 79 +++ programs/drift/src/state/perp_market.rs | 86 ++- programs/drift/src/state/spot_market.rs | 28 +- programs/drift/src/state/user.rs | 8 + programs/drift/src/validation/order.rs | 5 + sdk/VERSION | 2 +- sdk/package.json | 2 +- sdk/src/accounts/types.ts | 4 + .../webSocketDriftClientAccountSubscriber.ts | 2 +- sdk/src/adminClient.ts | 70 +++ sdk/src/constants/perpMarkets.ts | 20 + sdk/src/constants/spotMarkets.ts | 10 + sdk/src/driftClient.ts | 221 ++++++-- sdk/src/driftClientConfig.ts | 1 + sdk/src/events/webSocketLogProvider.ts | 13 +- sdk/src/idl/drift.json | 140 ++++- sdk/src/math/exchangeStatus.ts | 9 + sdk/src/math/position.ts | 6 +- .../priorityFee/averageOverSlotsStrategy.ts | 3 +- sdk/src/priorityFee/averageStrategy.ts | 3 +- sdk/src/priorityFee/ewmaStrategy.ts | 3 +- .../priorityFee/heliusPriorityFeeMethod.ts | 51 ++ sdk/src/priorityFee/index.ts | 2 + sdk/src/priorityFee/maxOverSlotsStrategy.ts | 3 +- sdk/src/priorityFee/priorityFeeSubscriber.ts | 188 +++++-- .../priorityFee/solanaPriorityFeeMethod.ts | 28 + sdk/src/priorityFee/types.ts | 32 +- sdk/src/tx/baseTxSender.ts | 33 +- sdk/src/tx/retryTxSender.ts | 21 +- sdk/src/tx/types.ts | 10 +- sdk/src/tx/utils.ts | 32 ++ sdk/src/types.ts | 14 + sdk/src/user.ts | 26 +- sdk/src/userMap/userMap.ts | 55 +- sdk/tests/amm/test.ts | 234 ++++++++- sdk/tests/tx/priorityFeeStrategy.ts | 4 +- test-scripts/run-anchor-tests.sh | 1 + test-scripts/single-anchor-test.sh | 2 +- tests/perpLpRiskMitigation.ts | 492 ++++++++++++++++++ 70 files changed, 3345 insertions(+), 438 deletions(-) create mode 100644 programs/drift/src/state/paused_operations.rs create mode 100644 programs/drift/src/state/paused_operations/tests.rs create mode 100644 sdk/src/priorityFee/heliusPriorityFeeMethod.ts create mode 100644 sdk/src/priorityFee/solanaPriorityFeeMethod.ts create mode 100644 tests/perpLpRiskMitigation.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a20c81e1a..46d87760e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Features + +### Fixes + +### Breaking + + +## [2.59.0] - 2023-01-30 + +### Features + +- program: separate out paused operations from market status ([#839](https://github.com/drift-labs/protocol-v2/pull/839)) +- program: use decayed last_oracle_conf_pct as lower bound for update ([#840](https://github.com/drift-labs/protocol-v2/pull/840)) + +### Fixes + +### Breaking + +## [2.58.0] - 2023-01-27 + +### Features + +### Fixes + +- program: AmmPaused doesnt block all fills + +### Breaking + +## [2.57.0] - 2023-01-25 + +### Features + +- program: add recenter amm ix ([#836](https://github.com/drift-labs/protocol-v2/pull/836)) + +### Fixes + +### Breaking + +## [2.56.0] - 2023-01-24 + +### Features + +### Fixes + +- program: enable jit maker to fill same slot as taker placed ([#835](https://github.com/drift-labs/protocol-v2/pull/835)) + +### Breaking + +## [2.55.0] - 2023-01-18 + +### Features + +### Fixes + +- program: standardize lp shares in attempt_burn_user_lp_shares_for_risk_reduction ([#826](https://github.com/drift-labs/protocol-v2/pull/826)) + +### Breaking + +## [2.54.0] - 2023-01-15 + ### Features - sdk: move bracket orders into single instruction - sdk: add ability to do placeAndTake order with bracket orders attached - sdk: add option to cancel existing orders in market for place and take order - +- sdk: add option to get signed settlePnl tx back from a market order +- program: auto derisk lp positions in settle pnl ([#766](https://github.com/drift-labs/protocol-v2/pull/766)) - program: increase full perp liquidation threshold ([#807](https://github.com/drift-labs/protocol-v2/pull/807)) - program: remove spot fee pool transfer ([#800](https://github.com/drift-labs/protocol-v2/pull/800)) - program: increase insurance tier max ([#784](https://github.com/drift-labs/protocol-v2/pull/784)) +- sdk: can specify max custom margin ratio to initialize a new account with ### Fixes diff --git a/Cargo.lock b/Cargo.lock index ec1e4d90d..374f401fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,7 +642,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.53.0" +version = "2.59.0" dependencies = [ "anchor-lang", "anchor-spl", @@ -1232,7 +1232,7 @@ dependencies = [ [[package]] name = "phoenix-v1" version = "0.2.4" -source = "git+https://github.com/drift-labs/phoenix-v1?rev=4c65c9#4c65c97cd62493e27425ea2830447c833152bed1" +source = "git+https://github.com/drift-labs/phoenix-v1?rev=bf6b84#bf6b8447047aa16485d8d976528e43f5d53331d9" dependencies = [ "borsh", "bytemuck", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 055b7707c..904cbdad0 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.53.0" +version = "2.59.0" description = "Created with Anchor" edition = "2018" @@ -31,7 +31,7 @@ arrayref = "0.3.6" base64 = "0.13.0" serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "85b4f14", version = "0.5.6", features = ["no-entrypoint"] } enumflags2 = "0.6.4" -phoenix-v1 = { git = "https://github.com/drift-labs/phoenix-v1", rev = "4c65c9", version = "0.2.4", features = ["no-entrypoint"] } +phoenix-v1 = { git = "https://github.com/drift-labs/phoenix-v1", rev = "bf6b84", version = "0.2.4", features = ["no-entrypoint"] } solana-security-txt = "1.1.0" static_assertions = "1.1.0" drift-macros = { git = "https://github.com/drift-labs/drift-macros.git", rev = "c57d87" } diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 9b253ac2b..cb84f5b05 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -745,10 +745,63 @@ pub fn move_price( validate!( (quote_asset_reserve.cast::()? - amm.quote_asset_reserve.cast::()?).abs() < 100, ErrorCode::InvalidAmmDetected, + "quote_asset_reserve passed doesnt reconcile enough {} vs {}", + quote_asset_reserve.cast::()?, + amm.quote_asset_reserve.cast::()? + )?; + + amm.sqrt_k = sqrt_k; + + let (_, terminal_quote_reserves, terminal_base_reserves) = + amm::calculate_terminal_price_and_reserves(amm)?; + amm.terminal_quote_asset_reserve = terminal_quote_reserves; + + let (min_base_asset_reserve, max_base_asset_reserve) = + amm::calculate_bid_ask_bounds(amm.concentration_coef, terminal_base_reserves)?; + + amm.max_base_asset_reserve = max_base_asset_reserve; + amm.min_base_asset_reserve = min_base_asset_reserve; + + let reserve_price_after = amm.reserve_price()?; + update_spreads(amm, reserve_price_after)?; + + Ok(()) +} + +// recenter peg with balanced terminal reserves +pub fn recenter_perp_market_amm(amm: &mut AMM, peg_multiplier: u128, sqrt_k: u128) -> DriftResult { + // calculate base/quote reserves for balanced terminal reserves + let swap_direction = if amm.base_asset_amount_with_amm > 0 { + SwapDirection::Remove + } else { + SwapDirection::Add + }; + let (new_quote_asset_amount, new_base_asset_amount) = amm::calculate_swap_output( + amm.base_asset_amount_with_amm.unsigned_abs(), + sqrt_k, + swap_direction, + sqrt_k, + )?; + + amm.base_asset_reserve = new_base_asset_amount; + + let k = bn::U256::from(sqrt_k).safe_mul(bn::U256::from(sqrt_k))?; + + amm.quote_asset_reserve = k + .safe_div(bn::U256::from(new_base_asset_amount))? + .try_to_u128()?; + + validate!( + (new_quote_asset_amount.cast::()? - amm.quote_asset_reserve.cast::()?).abs() + < 100, + ErrorCode::InvalidAmmDetected, "quote_asset_reserve passed doesnt reconcile enough" )?; amm.sqrt_k = sqrt_k; + // todo: could calcualte terminal state cost for altering sqrt_k + + amm.peg_multiplier = peg_multiplier; let (_, terminal_quote_reserves, terminal_base_reserves) = amm::calculate_terminal_price_and_reserves(amm)?; diff --git a/programs/drift/src/controller/lp.rs b/programs/drift/src/controller/lp.rs index 49ac59987..3bf13cba9 100644 --- a/programs/drift/src/controller/lp.rs +++ b/programs/drift/src/controller/lp.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::{msg, Pubkey}; use crate::bn::U192; use crate::controller; -use crate::controller::position::PositionDelta; +use crate::controller::position::{get_position_index, PositionDelta}; use crate::controller::position::{update_position_and_market, update_quote_asset_amount}; use crate::emit; use crate::error::{DriftResult, ErrorCode}; @@ -357,8 +357,13 @@ pub fn remove_perp_lp_shares( market_index: u16, now: i64, ) -> DriftResult<()> { + let position_index = get_position_index(&user.perp_positions, market_index)?; + // standardize n shares to burn - let shares_to_burn: u64 = { + // 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()?, @@ -382,7 +387,7 @@ pub fn remove_perp_lp_shares( controller::funding::settle_funding_payment(user, &user_key, &mut market, now)?; - let position = user.get_perp_position_mut(market_index)?; + let position = &mut user.perp_positions[position_index]; validate!( position.lp_shares >= shares_to_burn, diff --git a/programs/drift/src/controller/lp/tests.rs b/programs/drift/src/controller/lp/tests.rs index 277cbe24b..a92f3fdc7 100644 --- a/programs/drift/src/controller/lp/tests.rs +++ b/programs/drift/src/controller/lp/tests.rs @@ -32,6 +32,8 @@ 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 { @@ -433,8 +435,14 @@ pub fn test_lp_settle_pnl() { &pyth_program, oracle_account_info ); - let slot = 0; - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + 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 { @@ -516,8 +524,6 @@ pub fn test_lp_settle_pnl() { ..User::default() }; - let now = 1000000; - let state = State { oracle_guard_rails: OracleGuardRails { validity: ValidityGuardRails { @@ -555,7 +561,7 @@ pub fn test_lp_settle_pnl() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ); @@ -728,16 +734,21 @@ fn test_lp_margin_calc() { 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) = - calculate_perp_position_value_and_pnl( - &user.perp_positions[0], - &market, - &oracle_price_data, - &strict_quote_price, - crate::math::margin::MarginRequirementType::Initial, - 0, - ) - .unwrap(); + let ( + margin_requirement, + weighted_unrealized_pnl, + worse_case_base_asset_value, + _open_order_fraction, + ) = calculate_perp_position_value_and_pnl( + &user.perp_positions[0], + &market, + &oracle_price_data, + &strict_quote_price, + crate::math::margin::MarginRequirementType::Initial, + 0, + 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) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index c0913295c..7c3e9dc56 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1,6 +1,6 @@ use std::cell::RefMut; use std::collections::BTreeMap; -use std::ops::DerefMut; +use std::ops::{DerefMut, Div}; use std::u64; use anchor_lang::prelude::*; @@ -8,6 +8,7 @@ use solana_program::msg; use crate::controller; 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, @@ -55,13 +56,16 @@ use crate::math::amm::calculate_amm_available_liquidity; use crate::math::safe_unwrap::SafeUnwrap; use crate::math::spot_swap::select_margin_type_for_swap; use crate::print_error; -use crate::state::events::{emit_stack, get_order_action_record, OrderActionRecord, OrderRecord}; +use crate::state::events::{ + emit_stack, get_order_action_record, LPAction, LPRecord, OrderActionRecord, OrderRecord, +}; use crate::state::events::{OrderAction, OrderActionExplanation}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; -use crate::state::margin_calculation::MarginContext; +use crate::state::margin_calculation::{MarginCalculation, MarginContext}; use crate::state::oracle::{OraclePriceData, StrictOraclePrice}; use crate::state::oracle_map::OracleMap; +use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::{AMMLiquiditySplit, MarketStatus, PerpMarket}; use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_fulfillment_params::{ExternalSpotFill, SpotFulfillmentParams}; @@ -91,7 +95,8 @@ mod amm_lp_jit_tests; pub fn place_perp_order( state: &State, - user: &AccountLoader, + user: &mut User, + user_key: Pubkey, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -101,8 +106,6 @@ pub fn place_perp_order( ) -> DriftResult { let now = clock.unix_timestamp; let slot = clock.slot; - let user_key = user.key(); - let user = &mut load_mut!(user)?; validate_user_not_being_liquidated( user, @@ -176,7 +179,7 @@ pub fn place_perp_order( )?; validate!( - market.is_active(now)?, + !market.is_in_settlement(now), ErrorCode::MarketPlaceOrderPaused, "Market is in settlement mode", )?; @@ -748,15 +751,14 @@ pub fn modify_order( user.update_last_active_slot(clock.slot); - drop(user); - let order_params = merge_modify_order_params_with_existing_order(&existing_order, &modify_order_params)?; if order_params.market_type == MarketType::Perp { place_perp_order( state, - user_loader, + &mut user, + user_key, perp_market_map, spot_market_map, oracle_map, @@ -767,7 +769,8 @@ pub fn modify_order( } else { place_spot_order( state, - user_loader, + &mut user, + user_key, perp_market_map, spot_market_map, oracle_map, @@ -910,13 +913,16 @@ pub fn fill_perp_order( validate!( matches!( market.status, - MarketStatus::Active - | MarketStatus::FundingPaused - | MarketStatus::ReduceOnly - | MarketStatus::WithdrawPaused + MarketStatus::Active | MarketStatus::ReduceOnly ), ErrorCode::MarketFillOrderPaused, - "Market unavailable for fills" + "Market not active", + )?; + + validate!( + !market.is_operation_paused(PerpOperation::Fill), + ErrorCode::MarketFillOrderPaused, + "Market fills paused", )?; drop(market); @@ -960,10 +966,10 @@ pub fn fill_perp_order( let mut amm_is_available = !state.amm_paused()?; { let market = &mut perp_market_map.get_ref_mut(&market_index)?; - amm_is_available &= market.status != MarketStatus::AmmPaused; + amm_is_available &= !market.is_operation_paused(PerpOperation::AmmFill); validation::perp_market::validate_perp_market(market)?; validate!( - market.is_active(now)?, + !market.is_in_settlement(now), ErrorCode::MarketFillOrderPaused, "Market is in settlement mode", )?; @@ -1201,7 +1207,7 @@ pub fn fill_perp_order( { let market = &mut perp_market_map.get_ref_mut(&market_index)?; let funding_paused = - state.funding_paused()? || matches!(market.status, MarketStatus::FundingPaused); + state.funding_paused()? || market.is_operation_paused(PerpOperation::UpdateFunding); controller::funding::update_funding_rate( market_index, @@ -2706,7 +2712,7 @@ fn update_trigger_order_params( pub fn force_cancel_orders( state: &State, - user: &AccountLoader, + user_account_loader: &AccountLoader, spot_market_map: &SpotMarketMap, perp_market_map: &PerpMarketMap, oracle_map: &mut OracleMap, @@ -2717,8 +2723,8 @@ pub fn force_cancel_orders( let slot = clock.slot; let filler_key = filler.key(); - let user_key = user.key(); - let user = &mut load_mut!(user)?; + let user_key = user_account_loader.key(); + let user = &mut load_mut!(user_account_loader)?; let filler = &mut load_mut!(filler)?; validate!( @@ -2728,8 +2734,15 @@ pub fn force_cancel_orders( validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; - let meets_initial_margin_requirement = - meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?; + 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), + )?; + + let meets_initial_margin_requirement = margin_calc.meets_margin_requirement(); validate!( !meets_initial_margin_requirement, @@ -2818,6 +2831,119 @@ 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, + margin_calc: MarginCalculation, + user_key: Pubkey, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + clock: &Clock, + market_index: u16, +) -> DriftResult { + let now = clock.unix_timestamp; + // attempt to burn lp shares if user has a custom margin ratio set and its breached with orders + if !margin_calc.positions_meets_margin_requirement()? { + 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, + 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, + 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 lp_shares = user.perp_positions[position_index].lp_shares; + + let mut market = perp_market_map.get_ref_mut(&market_index)?; + let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?; + + let oracle_price = if market.status == MarketStatus::Settlement { + market.expiry_price + } else { + oracle_price_data.price + }; + + let order_step_size = market.amm.order_step_size; + + let lp_shares_to_burn = + standardize_base_asset_amount(lp_shares.div(3), order_step_size)?.max(lp_shares); + + 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::RemoveLiquidity, + 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, + user.perp_positions[position_index] + .base_asset_amount + .unsigned_abs(), + )?; + + drop(market); + + controller::orders::place_perp_order( + state, + user, + user_key, + perp_market_map, + spot_market_map, + oracle_map, + clock, + params, + PlaceOrderOptions::default(), + )?; + + Ok(()) +} + pub fn pay_keeper_flat_reward_for_perps( user: &mut User, filler: Option<&mut User>, @@ -2893,7 +3019,8 @@ pub fn pay_keeper_flat_reward_for_spot( pub fn place_spot_order( state: &State, - user: &AccountLoader, + user: &mut User, + user_key: Pubkey, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -2903,8 +3030,6 @@ pub fn place_spot_order( ) -> DriftResult { let now = clock.unix_timestamp; let slot = clock.slot; - let user_key = user.key(); - let user = &mut load_mut!(user)?; validate_user_not_being_liquidated( user, diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index e39bdaea6..df31db349 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -1,6 +1,9 @@ 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::{ + attempt_burn_user_lp_shares_for_risk_reduction, 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, @@ -12,13 +15,18 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::calculate_net_user_pnl; use crate::math::casting::Cast; -use crate::math::margin::meets_maintenance_margin_requirement; +use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, + meets_maintenance_margin_requirement, MarginRequirementType, +}; use crate::math::position::calculate_base_asset_value_with_expiry_price; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; +use crate::state::margin_calculation::MarginContext; use crate::state::events::{OrderActionExplanation, SettlePnlExplanation, SettlePnlRecord}; use crate::state::oracle_map::OracleMap; +use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalance, SpotBalanceType}; @@ -45,11 +53,11 @@ pub fn settle_pnl( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - now: i64, + clock: &Clock, state: &State, ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; - + let now = clock.unix_timestamp; { let spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; update_spot_market_cumulative_interest(spot_market, None, now)?; @@ -68,15 +76,43 @@ 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 unrealized_pnl < 0 - && !meets_maintenance_margin_requirement( + if user.perp_positions[position_index].is_lp() { + let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( user, perp_market_map, spot_market_map, oracle_map, - )? - { - return Err(ErrorCode::InsufficientCollateralForSettlingPNL); + MarginContext::standard(MarginRequirementType::Initial).track_open_orders_fraction()?, + )?; + + if !margin_calc.meets_margin_requirement() { + attempt_burn_user_lp_shares_for_risk_reduction( + state, + user, + margin_calc, + *user_key, + 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 { + return Ok(()); + } + } + } else if unrealized_pnl < 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, + )?) { + return Err(ErrorCode::InsufficientCollateralForSettlingPNL); + } } let spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; @@ -105,6 +141,20 @@ pub fn settle_pnl( "Cannot settle pnl under current market status" )?; + validate!( + !perp_market.is_operation_paused(PerpOperation::SettlePnl), + ErrorCode::InvalidMarketStatusToSettlePnl, + "Cannot settle pnl under current market status" + )?; + + if user.perp_positions[position_index].base_asset_amount != 0 { + validate!( + !perp_market.is_operation_paused(PerpOperation::SettlePnlWithPosition), + ErrorCode::InvalidMarketStatusToSettlePnl, + "Cannot settle pnl with position under current market status" + )?; + } + let pnl_pool_token_amount = get_token_amount( perp_market.pnl_pool.scaled_balance, spot_market, @@ -211,8 +261,7 @@ pub fn settle_expired_position( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, - now: i64, - slot: u64, + clock: &Clock, state: &State, ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -224,6 +273,8 @@ pub fn settle_expired_position( } let fee_structure = &state.perp_fee_structure; + let now = clock.unix_timestamp; + let slot = clock.slot; { let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index c20999908..7d7b860ab 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -142,12 +142,12 @@ pub mod delisting_test { ) .is_err()); assert_eq!(market.is_reduce_only().unwrap(), false); - assert_eq!(market.is_active(clock.unix_timestamp).unwrap(), true); + assert_eq!(market.is_in_settlement(clock.unix_timestamp), false); market.expiry_ts = clock.unix_timestamp + 100; assert_eq!(clock.unix_timestamp, 1662065595); - assert_eq!(market.is_active(clock.unix_timestamp).unwrap(), true); + assert_eq!(market.is_in_settlement(clock.unix_timestamp), false); assert_eq!(market.is_reduce_only().unwrap(), false); // isnt set like in update expiry ix market.status = MarketStatus::ReduceOnly; @@ -255,7 +255,7 @@ pub mod delisting_test { assert_eq!(market.expiry_ts < clock.unix_timestamp, true); assert_eq!(market.status, MarketStatus::Initialized); assert_eq!(market.expiry_price, 0); - assert_eq!(market.is_active(clock.unix_timestamp).unwrap(), false); + assert_eq!(market.is_in_settlement(clock.unix_timestamp), true); // put in settlement mode settle_expired_market( @@ -808,8 +808,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); @@ -1027,8 +1026,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); @@ -1233,8 +1231,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); @@ -1556,8 +1553,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); @@ -1615,8 +1611,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); @@ -1654,8 +1649,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); @@ -1912,7 +1906,7 @@ pub mod delisting_test { assert_eq!(total_collateral_short, 17_000_000_000); assert_eq!(margin_requirement_short, 16002510000); - assert_eq!(market.is_active(clock.unix_timestamp).unwrap(), false); + assert_eq!(market.is_in_settlement(clock.unix_timestamp), true); assert_eq!(market.is_reduce_only().unwrap(), false); // put in settlement mode @@ -1926,7 +1920,7 @@ pub mod delisting_test { ) .unwrap(); assert_eq!(market.is_reduce_only().unwrap(), false); - assert_eq!(market.is_active(clock.unix_timestamp).unwrap(), false); + assert_eq!(market.is_in_settlement(clock.unix_timestamp), true); let market = market_map.get_ref_mut(&0).unwrap(); assert_eq!(market.expiry_price != 0, true); @@ -1968,8 +1962,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state ) .is_err(), @@ -2343,15 +2336,17 @@ pub mod delisting_test { let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle).unwrap(); let strict_quote_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (perp_margin_requirement, weighted_pnl, _) = calculate_perp_position_value_and_pnl( - &shorter.perp_positions[0], - &market, - oracle_price_data, - &strict_quote_price, - MarginRequirementType::Initial, - 0, - ) - .unwrap(); + let (perp_margin_requirement, weighted_pnl, _, _) = + calculate_perp_position_value_and_pnl( + &shorter.perp_positions[0], + &market, + oracle_price_data, + &strict_quote_price, + MarginRequirementType::Initial, + 0, + false, + ) + .unwrap(); // short cant pay without bankruptcy assert_eq!(oracle_price_data.price, 100000000); @@ -2368,8 +2363,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .is_err()); @@ -2421,7 +2415,7 @@ pub mod delisting_test { let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle).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, @@ -2429,6 +2423,7 @@ pub mod delisting_test { &strict_quote_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -2507,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, @@ -2515,6 +2510,7 @@ pub mod delisting_test { &strict_quote_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -2597,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, @@ -2605,6 +2601,7 @@ pub mod delisting_test { &strict_quote_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -2681,8 +2678,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); @@ -2807,8 +2803,7 @@ pub mod delisting_test { &market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, &state, ) .unwrap(); diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index edfd35da5..753ac92ec 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -24,11 +24,16 @@ use crate::state::state::{OracleGuardRails, State, ValidityGuardRails}; use crate::state::user::{PerpPosition, SpotPosition, User}; use crate::test_utils::*; use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; - +use anchor_lang::prelude::Clock; #[test] pub fn user_no_position() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -53,7 +58,7 @@ pub fn user_no_position() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -129,7 +134,7 @@ pub fn user_no_position() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ); @@ -138,8 +143,13 @@ pub fn user_no_position() { #[test] pub fn user_does_not_meet_maintenance_requirement() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -164,7 +174,7 @@ pub fn user_does_not_meet_maintenance_requirement() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -247,7 +257,7 @@ pub fn user_does_not_meet_maintenance_requirement() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ); @@ -256,8 +266,13 @@ pub fn user_does_not_meet_maintenance_requirement() { #[test] pub fn user_unsettled_negative_pnl() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -280,7 +295,7 @@ pub fn user_unsettled_negative_pnl() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -375,7 +390,7 @@ pub fn user_unsettled_negative_pnl() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -386,8 +401,13 @@ pub fn user_unsettled_negative_pnl() { #[test] pub fn user_unsettled_positive_pnl_more_than_pool() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -410,7 +430,7 @@ pub fn user_unsettled_positive_pnl_more_than_pool() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -503,7 +523,7 @@ pub fn user_unsettled_positive_pnl_more_than_pool() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -514,8 +534,13 @@ pub fn user_unsettled_positive_pnl_more_than_pool() { #[test] pub fn user_unsettled_positive_pnl_less_than_pool() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -538,7 +563,7 @@ pub fn user_unsettled_positive_pnl_less_than_pool() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -633,7 +658,7 @@ pub fn user_unsettled_positive_pnl_less_than_pool() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -644,8 +669,14 @@ pub fn user_unsettled_positive_pnl_less_than_pool() { #[test] pub fn market_fee_pool_receives_portion() { - let now = 0_i64; - let slot = 0; + let clock = Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let slot = clock.slot; let state = State { oracle_guard_rails: OracleGuardRails { validity: ValidityGuardRails { @@ -765,7 +796,7 @@ pub fn market_fee_pool_receives_portion() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -776,8 +807,13 @@ pub fn market_fee_pool_receives_portion() { #[test] pub fn market_fee_pool_pays_back_to_pnl_pool() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -800,7 +836,7 @@ pub fn market_fee_pool_pays_back_to_pnl_pool() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -902,7 +938,7 @@ pub fn market_fee_pool_pays_back_to_pnl_pool() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -913,8 +949,13 @@ pub fn market_fee_pool_pays_back_to_pnl_pool() { #[test] pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -937,7 +978,7 @@ pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -1033,7 +1074,7 @@ pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -1044,8 +1085,13 @@ pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl() { #[test] pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl_price_breached() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -1068,7 +1114,7 @@ pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl_price_breached() &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -1164,7 +1210,7 @@ pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl_price_breached() &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .is_err()); @@ -1172,8 +1218,13 @@ pub fn user_long_positive_unrealized_pnl_up_to_max_positive_pnl_price_breached() #[test] pub fn user_long_negative_unrealized_pnl() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -1196,7 +1247,7 @@ pub fn user_long_negative_unrealized_pnl() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -1292,7 +1343,7 @@ pub fn user_long_negative_unrealized_pnl() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -1303,8 +1354,13 @@ pub fn user_long_negative_unrealized_pnl() { #[test] pub fn user_short_positive_unrealized_pnl_up_to_max_positive_pnl() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -1327,7 +1383,7 @@ pub fn user_short_positive_unrealized_pnl_up_to_max_positive_pnl() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -1423,7 +1479,7 @@ pub fn user_short_positive_unrealized_pnl_up_to_max_positive_pnl() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); @@ -1434,8 +1490,13 @@ pub fn user_short_positive_unrealized_pnl_up_to_max_positive_pnl() { #[test] pub fn user_short_negative_unrealized_pnl() { - let now = 0_i64; - let slot = 0_u64; + 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 { @@ -1458,7 +1519,7 @@ pub fn user_short_negative_unrealized_pnl() { &pyth_program, oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); let mut market = PerpMarket { amm: AMM { @@ -1554,7 +1615,7 @@ pub fn user_short_negative_unrealized_pnl() { &market_map, &spot_market_map, &mut oracle_map, - now, + &clock, &state, ) .unwrap(); diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 77b3ce7c5..a74f79fec 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -1,3 +1,6 @@ +use crate::controller::amm::{ + calculate_base_swap_output_with_spread, move_price, recenter_perp_market_amm, swap_base_asset, +}; use crate::controller::position::{ update_lp_market_position, update_position_and_market, PositionDelta, }; @@ -6,15 +9,21 @@ use crate::controller::lp::{apply_lp_rebase_to_perp_market, settle_lp_position}; use crate::controller::repeg::_update_amm; use crate::math::constants::{ - AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, BASE_PRECISION_I64, PRICE_PRECISION_I64, - PRICE_PRECISION_U64, QUOTE_PRECISION_I128, + AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, BASE_PRECISION, BASE_PRECISION_I64, + PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, }; +use crate::math::position::swap_direction_to_close_position; use crate::state::oracle::OraclePriceData; use crate::state::oracle_map::OracleMap; use crate::state::perp_market::{AMMLiquiditySplit, PerpMarket, AMM}; +use crate::state::perp_market_map::PerpMarketMap; use crate::state::state::State; use crate::state::user::PerpPosition; -use crate::test_utils::create_account_info; +use crate::test_utils::{create_account_info, get_account_bytes}; + +use crate::bn::U192; +use crate::math::cp_curve::{adjust_k_cost, get_update_k_result, update_k}; +use crate::test_utils::get_hardcoded_pyth_price; use anchor_lang::prelude::AccountLoader; use solana_program::pubkey::Pubkey; use std::str::FromStr; @@ -1383,9 +1392,49 @@ fn update_amm_near_boundary2() { let mut lamports = 0; let perp_market_account_info = create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); + let market_map = PerpMarketMap::load_one(&perp_market_account_info, true).unwrap(); - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); + let oracle_market_str = String::from("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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + let mut decoded_bytes = base64::decode(oracle_market_str).unwrap(); + let oracle_market_bytes = decoded_bytes.as_mut_slice(); + + let key = Pubkey::from_str("8ihFLu5FimgTQ1Unh4dVyEHUGodJ5gJQCrQf4KUVB9bN").unwrap(); + let owner = Pubkey::from_str("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH").unwrap(); + let mut lamports = 0; + let jto_market_account_info = + create_account_info(&key, true, &mut lamports, oracle_market_bytes, &owner); + + let slot = 234919073; + let now = 1702120657; + let mut oracle_map = OracleMap::load_one(&jto_market_account_info, slot, None).unwrap(); + + // let perp_market_old = market_map.get_ref(&4).unwrap(); + + let mut perp_market = market_map.get_ref_mut(&4).unwrap(); + + println!("perp_market: {:?}", perp_market.amm.last_update_slot); + + let oracle_price_data = oracle_map.get_price_data(&key).unwrap(); + + let state = State::default(); + + let cost = _update_amm(&mut perp_market, oracle_price_data, &state, now, slot).unwrap(); + + assert_eq!(cost, 2987010); +} + +#[test] +fn recenter_amm_1() { + let perp_market_str: String = String::from("Ct8MLGv1N/cU6tVVkVpIHdjrXil5+Blo7M7no01SEzFkvCN2nSnel3KwISF8o/5okioZqvmQEJy52E6a0AS00gJa1vUpMUQZIAjcAAAAAAAAAAAAAAAAAAEAAAAAAAAAuUnaAAAAAADDXNsAAAAAAP5xdGUAAAAAa4BQirD//////////////6fVQmsAAAAAAAAAAAAAAACar9SsB0sAAAAAAAAAAAAAAAAAAAAAAABBXO7/SWwLAAAAAAAAAAAAa0vYrBqvCwAAAAAAAAAAACaTDwAAAAAAAAAAAAAAAACHRTA1zkYLAAAAAAAAAAAAEkQuep2/CwAAAAAAAAAAAFAYOQmCjQsAAAAAAAAAAAC9r80AAAAAAAAAAAAAAAAANYB5EXeYCwAAAAAAAAAAAADqjJbciAAAAAAAAAAAAAAANiZLB47/////////////rEGjW00WAAAAAAAAAAAAAFTeD4aWAAAAAAAAAAAAAAAAQGNSv8YBAAAAAAAAAAAAUt/uyv7//////////////802zJqt/v/////////////PSTYa2wAAAAAAAAAAAAAAtPcalqL+/////////////xvHbwvuAAAAAAAAAAAAAAAAdsrWtPEAAAAAAAAAAAAAcbUT//////9xtRP//////3G1E///////Csx3AAAAAACVwjw2OgAAAAAAAAAAAAAAd/FNszYAAAAAAAAAAAAAALHQnZIDAAAAAAAAAAAAAAAA8z1QCQAAAAAAAAAAAAAAwY+XFgAAAAAAAAAAAAAAAEFTL9MIAAAAAAAAAAAAAAAHWeRpAAAAAAAAAAAAAAAAB1nkaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQlAeGCeEKAAAAAAAAAAAAME8Wz6hEDAAAAAAAAAAAABctSD9BbwsAAAAAAAAAAAA8T/PdEqwLAAAAAAAAAAAAMMvbAAAAAADpTP///////6NCywAAAAAA0yfeAAAAAAA7tdQAAAAAAJ3u2wAAAAAAwI8ADgAAAABrBAAAAAAAAA98N2D9////MTx0ZQAAAAAQDgAAAAAAAADKmjsAAAAAZAAAAAAAAAAA8gUqAQAAAAAAAAAAAAAA/9iJIUQBAAB7ga9oBQAAAADrzocBAAAAxXF0ZQAAAACI1QcAAAAAAHeBAQAAAAAA/nF0ZQAAAACUEQAAoIYBALV+AQDrBwAAAAAAAAAAAABkADIAZMgEAQAAAAAEAAAACvtTAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZUL9UG/wAAAAAAAAAAAAAAAAAAAAAAADFNQk9OSy1QRVJQICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHQNAgAAAAAA5xkAAAAAAACMAgAAAAAAACYCAADuAgAA+CQBAPgkAQDECQAA3AUAAAAAAAAQJwAAAgIAABwDAAAEAAIAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); + let perp_market_bytes = decoded_bytes.as_mut_slice(); + + let key = Pubkey::from_str("2QeqpeJUVo2LBWNELRfcBwJgrNoxJQSd7gokcaM5nvaa").unwrap(); + 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 market_map = PerpMarketMap::load_one(&perp_market_account_info, true).unwrap(); let oracle_market_str = String::from("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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); let mut decoded_bytes = base64::decode(oracle_market_str).unwrap(); @@ -1401,7 +1450,9 @@ fn update_amm_near_boundary2() { let now = 1702120657; let mut oracle_map = OracleMap::load_one(&jto_market_account_info, slot, None).unwrap(); - let mut perp_market = perp_market_loader.load_mut().unwrap(); + // let perp_market_old = market_map.get_ref(&4).unwrap(); + + let mut perp_market = market_map.get_ref_mut(&4).unwrap(); println!("perp_market: {:?}", perp_market.amm.last_update_slot); @@ -1412,4 +1463,260 @@ fn update_amm_near_boundary2() { let cost = _update_amm(&mut perp_market, oracle_price_data, &state, now, slot).unwrap(); assert_eq!(cost, 2987010); + + let inv = perp_market.amm.base_asset_amount_with_amm; + assert_eq!(inv, 24521505718700); + + let (_, _, r1_orig, r2_orig) = calculate_base_swap_output_with_spread( + &perp_market.amm, + inv.unsigned_abs() as u64, + swap_direction_to_close_position(inv), + ) + .unwrap(); + + assert_eq!(r1_orig, 334837204625); + assert_eq!(r2_orig, 703359043); + + let current_k = perp_market.amm.sqrt_k; + let _current_peg = perp_market.amm.peg_multiplier; + + let new_k = (current_k * 900000) / 100; + recenter_perp_market_amm(&mut perp_market.amm, oracle_price_data.price as u128, new_k).unwrap(); + + assert_eq!(perp_market.amm.sqrt_k, new_k); + assert_eq!( + perp_market.amm.peg_multiplier, + oracle_price_data.price as u128 + ); + + let (_r1, _r2) = swap_base_asset( + &mut perp_market, + inv.unsigned_abs() as u64, + swap_direction_to_close_position(inv), + ) + .unwrap(); + + // assert_eq!(r1, r1_orig); // 354919762322 w/o k adj + // assert_eq!(r2, r2_orig as i64); + + // assert_eq!(perp_market.amm.peg_multiplier, current_peg); +} + +#[test] +fn recenter_amm_2() { + // sui example + let perp_market_str: String = String::from("Ct8MLGv1N/d29jnnLxPJWcgnELd2ICWqe/HjfUfvrt/0yq7vt4ipySPXMVET9bHTunqDYExEuU159P1pr3f4BPx/kgptxldEbY8QAAAAAAAAAAAAAAAAAAMAAAAAAAAABb8QAAAAAADCjBAAAAAAANnvrmUAAAAAA/UzhKT1/////////////+zWKQkDAAAAAAAAAAAAAADXxsbXggQAAAAAAAAAAAAAAAAAAAAAAAAm1aGXXBcBAAAAAAAAAAAA0bqOq60ZeX0DAAAAAAAAADxrEgAAAAAAAAAAAAAAAABWUcGPbucAAAAAAAAAAAAAixe+mDdRAQAAAAAAAAAAAAHgQW8bmvMBAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAObJUKUBReX0DAAAAAAAAAAB82Wd71QAAAAAAAAAAAAAAvJautCf/////////////zNCf7v///////////////zRn0Ccw/f////////////8AAI1J/RoHAAAAAAAAAAAA2TrFMQwAAAAAAAAAAAAAAIasEJrH//////////////8CQy3yOAAAAAAAAAAAAAAA/Bzf4Mb//////////////9dAQLc5AAAAAAAAAAAAAAAA4EFvG5rzAQAAAAAAAAAA0Qb////////RBv///////9EG////////JaIAAAAAAADuHq3oAQAAAAAAAAAAAAAAZZBlmf///////////////2Y79WMCAAAAAAAAAAAAAACW6DzZ+f//////////////Ut/+OAEAAAAAAAAAAAAAAB0oBjUBAAAAAAAAAAAAAACR6S4LAAAAAAAAAAAAAAAAAOAtCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACn0WwwyBIBAAAAAAAAAAAAmOidoYFAXYwDAAAAAAAAAFSG6vGvFwEAAAAAAAAAAACRR6oTndNufAMAAAAAAAAAbosQAAAAAAAGdf///////1+cEAAAAAAARMEQAAAAAADRrhAAAAAAAH5MEAAAAAAA6EqDDgAAAADQAwAAAAAAAI007gAAAAAAQeauZQAAAAAQDgAAAAAAAADKmjsAAAAAZAAAAAAAAAAAypo7AAAAAAAAAAAAAAAAjPDu4DcAAAAXm1qdAAAAALcGYAwDAAAAiu6uZQAAAACqcwAAAAAAAJczAAAAAAAA2e+uZQAAAACIEwAAPHMAAOKBAAAYCQAAAAAAAKEHAABkADIAZMgAAQAAAAAEAAAATu+XBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3/spZrMwAAAAAAAAAAAAAAAAAAAAAAAFNVSS1QRVJQICAgICAgICAgICAgICAgICAgICAgICAgAOH1BQAAAAAA4fUFAAAAAADKmjsAAAAAiF7MCQAAAACH6a5lAAAAAADC6wsAAAAAAAAAAAAAAAAAAAAAAAAAAI0SAQAAAAAAbRgAAAAAAADDBgAAAAAAAMIBAADCAQAAECcAACBOAADoAwAA9AEAAAAAAAAQJwAAIAEAANEBAAAJAAEAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); + let perp_market_bytes = decoded_bytes.as_mut_slice(); + + let key = Pubkey::from_str("91NsaUmTNNdLGbYtwmoiYSn9SgWHCsZiChfMYMYZ2nQx").unwrap(); + 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 market_map = PerpMarketMap::load_one(&perp_market_account_info, true).unwrap(); + + // let oracle_market_str = String::from("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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + // let mut decoded_bytes = base64::decode(oracle_market_str).unwrap(); + // let oracle_market_bytes = decoded_bytes.as_mut_slice(); + + let mut oracle_price = get_hardcoded_pyth_price(1_120_000, 6); + let oracle_price_key = + Pubkey::from_str("3Qub3HaAJaa2xNY7SUqPKd3vVwTqDfDDkEUMPjXD2c1q").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + let mut data = get_account_bytes(&mut oracle_price); + let mut lamports2 = 0; + + let oracle_account_info = create_account_info( + &oracle_price_key, + true, + &mut lamports2, + &mut data[..], + &pyth_program, + ); + + //https://explorer.solana.com/block/243485436 + let slot = 243485436; + let now = 1705963488; + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + // let perp_market_old = market_map.get_ref(&4).unwrap(); + + let mut perp_market = market_map.get_ref_mut(&9).unwrap(); + + println!( + "perp_market latest slot: {:?}", + perp_market.amm.last_update_slot + ); + + // previous values + assert_eq!(perp_market.amm.peg_multiplier, 5); + assert_eq!(perp_market.amm.quote_asset_reserve, 64381518181749930705); + assert_eq!(perp_market.amm.base_asset_reserve, 307161425106214); + + let oracle_price_data = oracle_map.get_price_data(&oracle_price_key).unwrap(); + + let state = State::default(); + + let cost = _update_amm(&mut perp_market, oracle_price_data, &state, now, slot).unwrap(); + + assert_eq!(cost, 0); + + let inv = perp_market.amm.base_asset_amount_with_amm; + assert_eq!(inv, -291516212); + + let (_, _, r1_orig, r2_orig) = calculate_base_swap_output_with_spread( + &perp_market.amm, + inv.unsigned_abs() as u64, + swap_direction_to_close_position(inv), + ) + .unwrap(); + + assert_eq!(r1_orig, 326219); + assert_eq!(r2_orig, 20707); + + let current_k = perp_market.amm.sqrt_k; + let _current_peg = perp_market.amm.peg_multiplier; + let new_k = current_k * 2; + + // refusal to decrease further + assert_eq!(current_k, current_k); + assert_eq!(perp_market.amm.user_lp_shares, current_k - 1); + assert_eq!(perp_market.amm.get_lower_bound_sqrt_k().unwrap(), current_k); + + recenter_perp_market_amm(&mut perp_market.amm, oracle_price_data.price as u128, new_k).unwrap(); + + assert_eq!(perp_market.amm.sqrt_k, new_k); + assert_eq!( + perp_market.amm.peg_multiplier, + oracle_price_data.price as u128 + ); + assert_eq!(perp_market.amm.peg_multiplier, 1_120_000); + // assert_eq!(perp_market.amm.quote_asset_reserve, 140625455708483789 * 2); + // assert_eq!(perp_market.amm.base_asset_reserve, 140625456291516213 * 2); + assert_eq!(perp_market.amm.base_asset_reserve, 281250912291516214); + assert_eq!(perp_market.amm.quote_asset_reserve, 281250911708483790); + + crate::validation::perp_market::validate_perp_market(&perp_market).unwrap(); + + let (r1, r2) = swap_base_asset( + &mut perp_market, + inv.unsigned_abs() as u64, + swap_direction_to_close_position(inv), + ) + .unwrap(); + + // adjusted slightly + assert_eq!(r1, 348628); // 354919762322 w/o k adj + assert_eq!(r2, 22129); + + let new_scale = 2; + let new_sqrt_k = perp_market.amm.sqrt_k * new_scale; + let update_k_result = get_update_k_result(&perp_market, U192::from(new_sqrt_k), false).unwrap(); + let adjustment_cost = adjust_k_cost(&mut perp_market, &update_k_result).unwrap(); + assert_eq!(adjustment_cost, 0); + + update_k(&mut perp_market, &update_k_result).unwrap(); + + // higher lower bound now + assert_eq!(perp_market.amm.sqrt_k, new_sqrt_k); + assert_eq!(perp_market.amm.user_lp_shares, current_k - 1); + assert!(perp_market.amm.get_lower_bound_sqrt_k().unwrap() > current_k); + assert_eq!( + perp_market.amm.get_lower_bound_sqrt_k().unwrap(), + 140766081456000000 + ); + // assert_eq!(perp_market.amm.peg_multiplier, current_peg); +} + +#[test] +fn test_move_amm() { + // sui example + let perp_market_str: String = String::from("Ct8MLGv1N/d29jnnLxPJWcgnELd2ICWqe/HjfUfvrt/0yq7vt4ipySPXMVET9bHTunqDYExEuU159P1pr3f4BPx/kgptxldEbY8QAAAAAAAAAAAAAAAAAAMAAAAAAAAABb8QAAAAAADCjBAAAAAAANnvrmUAAAAAA/UzhKT1/////////////+zWKQkDAAAAAAAAAAAAAADXxsbXggQAAAAAAAAAAAAAAAAAAAAAAAAm1aGXXBcBAAAAAAAAAAAA0bqOq60ZeX0DAAAAAAAAADxrEgAAAAAAAAAAAAAAAABWUcGPbucAAAAAAAAAAAAAixe+mDdRAQAAAAAAAAAAAAHgQW8bmvMBAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAObJUKUBReX0DAAAAAAAAAAB82Wd71QAAAAAAAAAAAAAAvJautCf/////////////zNCf7v///////////////zRn0Ccw/f////////////8AAI1J/RoHAAAAAAAAAAAA2TrFMQwAAAAAAAAAAAAAAIasEJrH//////////////8CQy3yOAAAAAAAAAAAAAAA/Bzf4Mb//////////////9dAQLc5AAAAAAAAAAAAAAAA4EFvG5rzAQAAAAAAAAAA0Qb////////RBv///////9EG////////JaIAAAAAAADuHq3oAQAAAAAAAAAAAAAAZZBlmf///////////////2Y79WMCAAAAAAAAAAAAAACW6DzZ+f//////////////Ut/+OAEAAAAAAAAAAAAAAB0oBjUBAAAAAAAAAAAAAACR6S4LAAAAAAAAAAAAAAAAAOAtCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACn0WwwyBIBAAAAAAAAAAAAmOidoYFAXYwDAAAAAAAAAFSG6vGvFwEAAAAAAAAAAACRR6oTndNufAMAAAAAAAAAbosQAAAAAAAGdf///////1+cEAAAAAAARMEQAAAAAADRrhAAAAAAAH5MEAAAAAAA6EqDDgAAAADQAwAAAAAAAI007gAAAAAAQeauZQAAAAAQDgAAAAAAAADKmjsAAAAAZAAAAAAAAAAAypo7AAAAAAAAAAAAAAAAjPDu4DcAAAAXm1qdAAAAALcGYAwDAAAAiu6uZQAAAACqcwAAAAAAAJczAAAAAAAA2e+uZQAAAACIEwAAPHMAAOKBAAAYCQAAAAAAAKEHAABkADIAZMgAAQAAAAAEAAAATu+XBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3/spZrMwAAAAAAAAAAAAAAAAAAAAAAAFNVSS1QRVJQICAgICAgICAgICAgICAgICAgICAgICAgAOH1BQAAAAAA4fUFAAAAAADKmjsAAAAAiF7MCQAAAACH6a5lAAAAAADC6wsAAAAAAAAAAAAAAAAAAAAAAAAAAI0SAQAAAAAAbRgAAAAAAADDBgAAAAAAAMIBAADCAQAAECcAACBOAADoAwAA9AEAAAAAAAAQJwAAIAEAANEBAAAJAAEAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); + let perp_market_bytes = decoded_bytes.as_mut_slice(); + + let key = Pubkey::from_str("91NsaUmTNNdLGbYtwmoiYSn9SgWHCsZiChfMYMYZ2nQx").unwrap(); + 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 market_map = PerpMarketMap::load_one(&perp_market_account_info, true).unwrap(); + + // let oracle_market_str = String::from("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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + // let mut decoded_bytes = base64::decode(oracle_market_str).unwrap(); + // let oracle_market_bytes = decoded_bytes.as_mut_slice(); + + let mut oracle_price = get_hardcoded_pyth_price(1_120_000, 6); + let oracle_price_key = + Pubkey::from_str("3Qub3HaAJaa2xNY7SUqPKd3vVwTqDfDDkEUMPjXD2c1q").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + let mut data = get_account_bytes(&mut oracle_price); + let mut lamports2 = 0; + + let oracle_account_info = create_account_info( + &oracle_price_key, + true, + &mut lamports2, + &mut data[..], + &pyth_program, + ); + + //https://explorer.solana.com/block/243485436 + let slot = 243485436; + let now = 1705963488; + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + // let perp_market_old = market_map.get_ref(&4).unwrap(); + + let mut perp_market = market_map.get_ref_mut(&9).unwrap(); + + println!( + "perp_market latest slot: {:?}", + perp_market.amm.last_update_slot + ); + + // previous values + assert_eq!(perp_market.amm.peg_multiplier, 5); + assert_eq!(perp_market.amm.quote_asset_reserve, 64381518181749930705); + assert_eq!(perp_market.amm.base_asset_reserve, 307161425106214); + + let oracle_price_data = oracle_map.get_price_data(&oracle_price_key).unwrap(); + + let state = State::default(); + + let cost = _update_amm(&mut perp_market, oracle_price_data, &state, now, slot).unwrap(); + + assert_eq!(cost, 0); + + let inv = perp_market.amm.base_asset_amount_with_amm; + assert_eq!(inv, -291516212); + + let (_, _, r1_orig, r2_orig) = calculate_base_swap_output_with_spread( + &perp_market.amm, + inv.unsigned_abs() as u64, + swap_direction_to_close_position(inv), + ) + .unwrap(); + + assert_eq!(r1_orig, 326219); + assert_eq!(r2_orig, 20707); + let current_bar = perp_market.amm.base_asset_reserve; + let _current_qar = perp_market.amm.quote_asset_reserve; + let current_k = perp_market.amm.sqrt_k; + let inc_numerator = BASE_PRECISION + BASE_PRECISION / 100; + let new_k = current_k * inc_numerator / BASE_PRECISION; + + // test correction + move_price( + &mut perp_market.amm, + current_bar * inc_numerator / BASE_PRECISION, + // current_qar * inc_numerator / BASE_PRECISION, + 65025333363567459347, // pass in exact amount that reconciles + new_k, + ) + .unwrap(); + crate::validation::perp_market::validate_perp_market(&perp_market).unwrap(); + assert_eq!(perp_market.amm.sqrt_k, new_k); + assert_eq!(perp_market.amm.peg_multiplier, 5); // still same } diff --git a/programs/drift/src/controller/spot_balance.rs b/programs/drift/src/controller/spot_balance.rs index 84dffe484..47a3622b7 100644 --- a/programs/drift/src/controller/spot_balance.rs +++ b/programs/drift/src/controller/spot_balance.rs @@ -20,12 +20,12 @@ use crate::math::stats::{calculate_new_twap, calculate_weighted_average}; use crate::state::events::SpotInterestRecord; use crate::state::oracle::OraclePriceData; -use crate::state::perp_market::MarketStatus; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; use crate::validate; use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; use crate::math::safe_math::SafeMath; +use crate::state::paused_operations::SpotOperation; #[cfg(test)] mod tests; @@ -124,7 +124,7 @@ pub fn update_spot_market_cumulative_interest( oracle_price_data: Option<&OraclePriceData>, now: i64, ) -> DriftResult { - if spot_market.status == MarketStatus::FundingPaused { + if spot_market.is_operation_paused(SpotOperation::UpdateCumulativeInterest) { update_spot_market_twap_stats(spot_market, oracle_price_data, now)?; return Ok(()); } diff --git a/programs/drift/src/controller/spot_position.rs b/programs/drift/src/controller/spot_position.rs index 7c1846ba0..e213c8d97 100644 --- a/programs/drift/src/controller/spot_position.rs +++ b/programs/drift/src/controller/spot_position.rs @@ -9,6 +9,7 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_withdraw::check_withdraw_limits; use crate::safe_decrement; use crate::safe_increment; +use crate::state::paused_operations::SpotOperation; use crate::state::perp_market::MarketStatus; use crate::state::spot_market::{AssetTier, SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::user::{SpotPosition, User, UserStats}; @@ -127,14 +128,16 @@ pub fn update_spot_balances_and_cumulative_deposits_with_limits( validate!( matches!( spot_market.status, - MarketStatus::Active - | MarketStatus::AmmPaused - | MarketStatus::FundingPaused - | MarketStatus::FillPaused - | MarketStatus::ReduceOnly - | MarketStatus::Settlement + MarketStatus::Active | MarketStatus::ReduceOnly | MarketStatus::Settlement ), ErrorCode::MarketWithdrawPaused, + "Spot Market {} withdraws are currently paused, market not active or in settlement", + spot_market.market_index + )?; + + validate!( + !spot_market.is_operation_paused(SpotOperation::Withdraw), + ErrorCode::MarketWithdrawPaused, "Spot Market {} withdraws are currently paused", spot_market.market_index )?; diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 0e13d6645..e73b8ef8b 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -37,6 +37,7 @@ use crate::state::oracle::{ get_oracle_price, get_pyth_price, HistoricalIndexData, HistoricalOracleData, OraclePriceData, OracleSource, }; +use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::{ ContractTier, ContractType, InsuranceClaim, MarketStatus, PerpMarket, PoolBalance, AMM, }; @@ -266,7 +267,8 @@ pub fn handle_initialize_spot_market( spot_fee_pool: PoolBalance::default(), // in quote asset total_spot_fee: 0, orders_enabled: spot_market_index != 0, - padding1: [0; 6], + paused_operations: 0, + padding1: [0; 5], flash_loan_amount: 0, flash_loan_initial_token_amount: 0, total_swap_fee: 0, @@ -632,7 +634,7 @@ pub fn handle_initialize_perp_market( unrealized_pnl_max_imbalance: 0, liquidator_fee, if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, // 1% - padding1: 0, + paused_operations: 0, quote_spot_market_index: 0, fee_adjustment: 0, padding: [0; 46], @@ -867,6 +869,21 @@ pub fn handle_move_amm_price( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_recenter_perp_market_amm( + ctx: Context, + peg_multiplier: u128, + sqrt_k: u128, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + controller::amm::recenter_perp_market_amm(&mut perp_market.amm, peg_multiplier, sqrt_k)?; + validate_perp_market(perp_market)?; + + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -1166,7 +1183,7 @@ pub fn handle_update_k(ctx: Context, sqrt_k: u128) -> Result<()> { let update_k_result = get_update_k_result(perp_market, new_sqrt_k_u192, true)?; - let adjustment_cost = math::cp_curve::adjust_k_cost(perp_market, &update_k_result)?; + let adjustment_cost: i128 = math::cp_curve::adjust_k_cost(perp_market, &update_k_result)?; math::cp_curve::update_k(perp_market, &update_k_result)?; @@ -1618,11 +1635,27 @@ pub fn handle_update_spot_market_status( ctx: Context, status: MarketStatus, ) -> Result<()> { + status.validate_not_deprecated()?; let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; spot_market.status = status; Ok(()) } +#[access_control( +spot_market_valid(&ctx.accounts.spot_market) +)] +pub fn handle_update_spot_market_paused_operations( + ctx: Context, + paused_operations: u8, +) -> Result<()> { + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + spot_market.paused_operations = paused_operations; + + SpotOperation::log_all_operations_paused(spot_market.paused_operations); + + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] @@ -1741,11 +1774,28 @@ pub fn handle_update_perp_market_status( "must set settlement/delist through another instruction", )?; + status.validate_not_deprecated()?; + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; perp_market.status = status; Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_market_paused_operations( + ctx: Context, + paused_operations: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + perp_market.paused_operations = paused_operations; + + PerpOperation::log_all_operations_paused(perp_market.paused_operations); + + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 3290a914d..439c1e204 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -17,6 +17,7 @@ use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; use crate::state::fulfillment_params::serum::SerumFulfillmentParams; use crate::state::insurance_fund_stake::InsuranceFundStake; use crate::state::oracle_map::OracleMap; +use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::{MarketStatus, PerpMarket}; use crate::state::perp_market_map::{ get_market_set_for_user_positions, get_market_set_from_list, get_writable_perp_market_set, @@ -445,8 +446,7 @@ pub fn handle_settle_pnl(ctx: Context, market_index: u16) -> Result<( &perp_market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, - clock.slot, + &clock, state, )?; @@ -469,7 +469,7 @@ pub fn handle_settle_pnl(ctx: Context, market_index: u16) -> Result<( &perp_market_map, &spot_market_map, &mut oracle_map, - clock.unix_timestamp, + &clock, state, ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; @@ -888,7 +888,7 @@ pub fn handle_resolve_perp_pnl_deficit( } validate!( - perp_market.is_active(now)?, + !perp_market.is_in_settlement(now), ErrorCode::MarketActionPaused, "Market is in settlement mode", )?; @@ -1168,7 +1168,10 @@ pub fn handle_update_funding_rate( controller::repeg::_update_amm(perp_market, oracle_price_data, state, now, clock_slot)?; validate!( - matches!(perp_market.status, MarketStatus::Active), + matches!( + perp_market.status, + MarketStatus::Active | MarketStatus::ReduceOnly + ), ErrorCode::MarketActionPaused, "Market funding is paused", )?; @@ -1180,6 +1183,9 @@ pub fn handle_update_funding_rate( "AMM must be updated in a prior instruction within same slot" )?; + let funding_paused = + state.funding_paused()? || perp_market.is_operation_paused(PerpOperation::UpdateFunding); + let is_updated = controller::funding::update_funding_rate( perp_market_index, perp_market, @@ -1187,7 +1193,7 @@ pub fn handle_update_funding_rate( now, clock_slot, &state.oracle_guard_rails, - state.funding_paused()?, + funding_paused, None, )?; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 84ea8dc38..9a28ba10e 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -48,6 +48,7 @@ use crate::state::oracle::StrictOraclePrice; use crate::state::order_params::{ ModifyOrderParams, OrderParams, PlaceOrderOptions, PostOnlyParam, }; +use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::{get_writable_perp_market_set, MarketSet}; use crate::state::spot_fulfillment_params::SpotFulfillmentParams; @@ -342,16 +343,9 @@ pub fn handle_deposit( if spot_position.balance_type == SpotBalanceType::Deposit && spot_position.scaled_balance > 0 { validate!( - matches!( - spot_market.status, - MarketStatus::Active - | MarketStatus::FundingPaused - | MarketStatus::AmmPaused - | MarketStatus::FillPaused - | MarketStatus::WithdrawPaused - ), + matches!(spot_market.status, MarketStatus::Active), ErrorCode::MarketActionPaused, - "spot_market in reduce only mode", + "spot_market not active", )?; } @@ -647,21 +641,6 @@ pub fn handle_transfer_deposit( { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; - validate!( - matches!( - spot_market.status, - MarketStatus::Active - | MarketStatus::AmmPaused - | MarketStatus::FundingPaused - | MarketStatus::FillPaused - | MarketStatus::ReduceOnly - | MarketStatus::Settlement - ), - ErrorCode::MarketWithdrawPaused, - "Spot Market {} withdraws are currently paused", - spot_market.market_index - )?; - from_user.increment_total_withdraws( amount, oracle_price, @@ -809,9 +788,13 @@ pub fn handle_place_perp_order(ctx: Context, params: OrderParams) -> return Err(print_error!(ErrorCode::InvalidOrderIOC)().into()); } + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + controller::orders::place_perp_order( &ctx.accounts.state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1072,6 +1055,9 @@ pub fn handle_place_orders(ctx: Context, params: Vec) - "max 32 order params" )?; + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + let num_orders = params.len(); for (i, params) in params.iter().enumerate() { validate!( @@ -1090,7 +1076,8 @@ pub fn handle_place_orders(ctx: Context, params: Vec) - if params.market_type == MarketType::Perp { controller::orders::place_perp_order( &ctx.accounts.state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1101,7 +1088,8 @@ pub fn handle_place_orders(ctx: Context, params: Vec) - } else { controller::orders::place_spot_order( &ctx.accounts.state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1157,9 +1145,13 @@ pub fn handle_place_and_take_perp_order<'info>( &Clock::get()?, )?; + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + controller::orders::place_perp_order( &ctx.accounts.state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1168,6 +1160,8 @@ pub fn handle_place_and_take_perp_order<'info>( PlaceOrderOptions::default(), )?; + drop(user); + let user = &mut ctx.accounts.user; let order_id = load!(user)?.get_last_order_id(); @@ -1247,9 +1241,13 @@ pub fn handle_place_and_make_perp_order<'a, 'b, 'c, 'info>( clock, )?; + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + controller::orders::place_perp_order( state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1258,11 +1256,9 @@ pub fn handle_place_and_make_perp_order<'a, 'b, 'c, 'info>( PlaceOrderOptions::default(), )?; - let (order_id, authority) = { - let user = load!(ctx.accounts.user)?; - let order_id = user.get_last_order_id(); - (order_id, user.authority) - }; + let (order_id, authority) = (user.get_last_order_id(), user.authority); + + drop(user); let (mut makers_and_referrer, mut makers_and_referrer_stats) = load_user_maps(remaining_accounts_iter, true)?; @@ -1323,9 +1319,13 @@ pub fn handle_place_spot_order(ctx: Context, params: OrderParams) -> return Err(print_error!(ErrorCode::InvalidOrderIOC)().into()); } + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + controller::orders::place_spot_order( &ctx.accounts.state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1412,9 +1412,13 @@ pub fn handle_place_and_take_spot_order<'info>( } }; + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + controller::orders::place_spot_order( &ctx.accounts.state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1423,6 +1427,8 @@ pub fn handle_place_and_take_spot_order<'info>( PlaceOrderOptions::default(), )?; + drop(user); + let user = &mut ctx.accounts.user; let order_id = load!(user)?.get_last_order_id(); @@ -1536,9 +1542,13 @@ pub fn handle_place_and_make_spot_order<'info>( } }; + let user_key = ctx.accounts.user.key(); + let mut user = load_mut!(ctx.accounts.user)?; + controller::orders::place_spot_order( state, - &ctx.accounts.user, + &mut user, + user_key, &perp_market_map, &spot_market_map, &mut oracle_map, @@ -1547,6 +1557,8 @@ pub fn handle_place_and_make_spot_order<'info>( PlaceOrderOptions::default(), )?; + drop(user); + let order_id = load!(ctx.accounts.user)?.get_last_order_id(); controller::orders::fill_spot_order( @@ -1628,17 +1640,17 @@ pub fn handle_add_perp_lp_shares<'info>( let mut market = perp_market_map.get_ref_mut(&market_index)?; validate!( - matches!( - market.status, - MarketStatus::Active - | MarketStatus::FundingPaused - | MarketStatus::FillPaused - | MarketStatus::WithdrawPaused - ), + matches!(market.status, MarketStatus::Active), ErrorCode::MarketStatusInvalidForNewLP, "Market Status doesn't allow for new 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, @@ -1927,7 +1939,7 @@ pub fn handle_deposit_into_spot_market_revenue_pool( let mut spot_market = load_mut!(ctx.accounts.spot_market)?; validate!( - spot_market.is_active(Clock::get()?.unix_timestamp)?, + !spot_market.is_in_settlement(Clock::get()?.unix_timestamp), ErrorCode::DefaultError, "spot market {} not active", spot_market.market_index diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 53acdbdc3..40578e271 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -626,6 +626,14 @@ pub mod drift { handle_move_amm_price(ctx, base_asset_reserve, quote_asset_reserve, sqrt_k) } + pub fn recenter_perp_market_amm( + ctx: Context, + peg_multiplier: u128, + sqrt_k: u128, + ) -> Result<()> { + handle_recenter_perp_market_amm(ctx, peg_multiplier, sqrt_k) + } + pub fn update_perp_market_expiry( ctx: Context, expiry_ts: i64, @@ -744,6 +752,13 @@ pub mod drift { handle_update_spot_market_status(ctx, status) } + pub fn update_spot_market_paused_operations( + ctx: Context, + paused_operations: u8, + ) -> Result<()> { + handle_update_spot_market_paused_operations(ctx, paused_operations) + } + pub fn update_spot_market_asset_tier( ctx: Context, asset_tier: AssetTier, @@ -844,6 +859,13 @@ pub mod drift { handle_update_perp_market_status(ctx, status) } + pub fn update_perp_market_paused_operations( + ctx: Context, + paused_operations: u8, + ) -> Result<()> { + handle_update_perp_market_paused_operations(ctx, paused_operations) + } + pub fn update_perp_market_contract_tier( ctx: Context, contract_tier: ContractTier, diff --git a/programs/drift/src/math/amm.rs b/programs/drift/src/math/amm.rs index c5724ef73..33ad4f739 100644 --- a/programs/drift/src/math/amm.rs +++ b/programs/drift/src/math/amm.rs @@ -8,7 +8,7 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::bn::U192; use crate::math::casting::Cast; use crate::math::constants::{ - BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, CONCENTRATION_PRECISION, + 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, QUOTE_PRECISION_I64, @@ -428,10 +428,11 @@ pub fn update_oracle_price_twap( amm.last_oracle_normalised_price = capped_oracle_update_price; amm.historical_oracle_data.last_oracle_price = oracle_price_data.price; - amm.last_oracle_conf_pct = oracle_price_data - .confidence - .safe_mul(BID_ASK_SPREAD_PRECISION)? - .safe_div(reserve_price)? as u64; + + // use decayed last_oracle_conf_pct as lower bound + amm.last_oracle_conf_pct = + amm.get_new_oracle_conf_pct(oracle_price_data.confidence, reserve_price, now)?; + amm.historical_oracle_data.last_oracle_delay = oracle_price_data.delay; amm.last_oracle_reserve_price_spread_pct = calculate_oracle_reserve_price_spread_pct(amm, oracle_price_data, Some(reserve_price))?; diff --git a/programs/drift/src/math/amm/tests.rs b/programs/drift/src/math/amm/tests.rs index d1d0c0cd0..7af69d42f 100644 --- a/programs/drift/src/math/amm/tests.rs +++ b/programs/drift/src/math/amm/tests.rs @@ -806,3 +806,64 @@ fn calc_oracle_twap_clamp_update_tests() { ); assert_eq!(amm.last_oracle_normalised_price, 129_900_873); } + +#[test] +fn test_last_oracle_conf_update() { + let prev = 1667387000; + let now = prev + 1; + + let mut amm = AMM { + quote_asset_reserve: 200 * AMM_RESERVE_PRECISION, + base_asset_reserve: 200 * AMM_RESERVE_PRECISION, + peg_multiplier: 13 * PEG_PRECISION, + base_spread: 0, + long_spread: 0, + short_spread: 0, + last_mark_price_twap: (13 * PRICE_PRECISION_U64), + last_bid_price_twap: (13 * PRICE_PRECISION_U64), + last_ask_price_twap: (13 * PRICE_PRECISION_U64), + last_mark_price_twap_ts: prev, + funding_period: 3600, + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (13 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (13 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (13 * PRICE_PRECISION) as i64, + last_oracle_price_twap_ts: prev, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }; + + // price jumps 10x + let oracle_price_data = OraclePriceData { + price: 130 * PRICE_PRECISION_I64 + 873, + confidence: PRICE_PRECISION_U64 / 10, + delay: 1, + has_sufficient_number_of_data_points: true, + }; + + update_oracle_price_twap(&mut amm, now, &oracle_price_data, None, None).unwrap(); + + assert_eq!(amm.last_oracle_conf_pct, 7692); + + // price jumps 10x + let oracle_price_data = OraclePriceData { + price: 130 * PRICE_PRECISION_I64 + 873, + confidence: 1, + delay: 5, + has_sufficient_number_of_data_points: true, + }; + + // unchanged if now hasnt changed + update_oracle_price_twap(&mut amm, now, &oracle_price_data, None, None).unwrap(); + assert_eq!(amm.last_oracle_conf_pct, 7692); + + update_oracle_price_twap(&mut amm, now + 1, &oracle_price_data, None, None).unwrap(); + + assert_eq!(amm.last_oracle_conf_pct, 7692 - 7692 / 20); // 7287 + + // longer time between update means delay is faster + update_oracle_price_twap(&mut amm, now + 60, &oracle_price_data, None, None).unwrap(); + + assert_eq!(amm.last_oracle_conf_pct, 7307 - 7307 / 5 + 1); //5847 +} diff --git a/programs/drift/src/math/amm_spread.rs b/programs/drift/src/math/amm_spread.rs index ac66c2b6f..73a0d096f 100644 --- a/programs/drift/src/math/amm_spread.rs +++ b/programs/drift/src/math/amm_spread.rs @@ -332,9 +332,10 @@ pub fn calculate_spread( let mut long_spread = max(half_base_spread_u64, long_vol_spread); let mut short_spread = max(half_base_spread_u64, short_vol_spread); - let max_target_spread = max_spread - .cast::()? - .max(last_oracle_reserve_price_spread_pct.unsigned_abs()); + // todo add more baselines for max spread to increase + let max_spread_baseline = last_oracle_reserve_price_spread_pct.unsigned_abs(); + + let max_target_spread = max_spread.cast::()?.max(max_spread_baseline); // oracle retreat // if mark - oracle < 0 (mark below oracle) and user going long then increase spread diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index 284e4a338..86245b164 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -128,7 +128,7 @@ pub const ONE_YEAR: u128 = 31536000; pub const ONE_HUNDRED_MILLION_QUOTE: u64 = 100_000_000_u64 * QUOTE_PRECISION_U64; pub const FIFTY_MILLION_QUOTE: u64 = 50_000_000_u64 * QUOTE_PRECISION_U64; pub const TEN_MILLION_QUOTE: u64 = 10_000_000_u64 * QUOTE_PRECISION_U64; -pub const FIVE_MILLION_QUOTE: u64 = 10_000_000_u64 * QUOTE_PRECISION_U64; +pub const FIVE_MILLION_QUOTE: u64 = 5_000_000_u64 * QUOTE_PRECISION_U64; pub const ONE_MILLION_QUOTE: u64 = 1_000_000_u64 * QUOTE_PRECISION_U64; pub const TWO_HUNDRED_FIFTY_THOUSAND_QUOTE: u64 = 250_000_u64 * QUOTE_PRECISION_U64; pub const ONE_HUNDRED_THOUSAND_QUOTE: u64 = 100_000_u64 * QUOTE_PRECISION_U64; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 71d072e6c..4a7e85d0e 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -103,7 +103,8 @@ pub fn calculate_perp_position_value_and_pnl( strict_quote_price: &StrictOraclePrice, margin_requirement_type: MarginRequirementType, user_custom_margin_ratio: u32, -) -> DriftResult<(u128, i128, u128)> { + track_open_order_fraction: bool, +) -> DriftResult<(u128, i128, u128, u128)> { let valuation_price = if market.status == MarketStatus::Settlement { market.expiry_price } else { @@ -189,10 +190,22 @@ 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_base_asset_value, + open_order_margin_requirement, )) } @@ -440,15 +453,20 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( Some(DriftAction::MarginCalc), )?); - let (perp_margin_requirement, weighted_pnl, worst_case_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, - )?; + let ( + perp_margin_requirement, + weighted_pnl, + worst_case_base_asset_value, + open_order_margin_requirement, + ) = calculate_perp_position_value_and_pnl( + market_position, + market, + oracle_price_data, + &strict_quote_price, + context.margin_type, + user_custom_margin_ratio, + calculation.track_open_orders_fraction(), + )?; calculation.add_margin_requirement( perp_margin_requirement, @@ -456,6 +474,10 @@ 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)?; if market_position.base_asset_amount != 0 diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 1da61848d..c8b6b977b 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -273,13 +273,14 @@ 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, &strict_oracle_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -349,13 +350,14 @@ 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, &strict_oracle_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -412,13 +414,14 @@ mod test { }; let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, _, _) = calculate_perp_position_value_and_pnl( + let (pmr, _, _, _) = calculate_perp_position_value_and_pnl( &position, &market, &oracle_price_data, &strict_oracle_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -435,13 +438,14 @@ mod test { market.amm.quote_asset_reserve = new_qar; market.amm.base_asset_reserve = new_bar; - let (pmr2, _, _) = calculate_perp_position_value_and_pnl( + let (pmr2, _, _, _) = calculate_perp_position_value_and_pnl( &position, &market, &oracle_price_data, &strict_oracle_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -482,13 +486,14 @@ mod test { }; let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, _, _) = calculate_perp_position_value_and_pnl( + let (pmr, _, _, _) = calculate_perp_position_value_and_pnl( &position, &market, &oracle_price_data, &strict_oracle_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); @@ -505,13 +510,14 @@ mod test { market.amm.base_asset_reserve = new_bar; let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr2, _, _) = calculate_perp_position_value_and_pnl( + let (pmr2, _, _, _) = calculate_perp_position_value_and_pnl( &position, &market, &oracle_price_data, &strict_oracle_price, MarginRequirementType::Initial, 0, + false, ) .unwrap(); diff --git a/programs/drift/src/math/matching.rs b/programs/drift/src/math/matching.rs index 0e3eb686b..ece67124d 100644 --- a/programs/drift/src/math/matching.rs +++ b/programs/drift/src/math/matching.rs @@ -18,7 +18,7 @@ pub fn is_maker_for_taker( slot: u64, ) -> DriftResult { // Maker and taker order not allowed to match if both were placed in the current slot - if slot == maker_order.slot && slot == taker_order.slot { + if slot == maker_order.slot && slot == taker_order.slot && !maker_order.is_jit_maker() { return Ok(false); }; diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index 9a10cd9ac..30f2a7ebb 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -10,7 +10,8 @@ use crate::math::constants::BID_ASK_SPREAD_PRECISION; use crate::math::safe_math::SafeMath; use crate::state::oracle::OraclePriceData; -use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; +use crate::state::paused_operations::PerpOperation; +use crate::state::perp_market::{PerpMarket, AMM}; use crate::state::state::{OracleGuardRails, ValidityGuardRails}; #[cfg(test)] @@ -123,7 +124,7 @@ pub fn block_operation( let slots_since_amm_update = slot.saturating_sub(market.amm.last_update_slot); - let funding_paused_on_market = market.status == MarketStatus::FundingPaused; + let funding_paused_on_market = market.is_operation_paused(PerpOperation::UpdateFunding); let block = slots_since_amm_update > 10 || !is_oracle_valid diff --git a/programs/drift/src/math/repeg.rs b/programs/drift/src/math/repeg.rs index 0f9568031..54395f149 100644 --- a/programs/drift/src/math/repeg.rs +++ b/programs/drift/src/math/repeg.rs @@ -284,11 +284,14 @@ pub fn adjust_amm( let adjustment_cost: i128 = if adjust_k && can_lower_k { // TODO can be off by 1? + // always let protocol-owned sqrt_k be either least .1% of lps or the base amount / min order + let new_sqrt_k_lower_bound = market.amm.get_lower_bound_sqrt_k()?; + let new_sqrt_k = market .amm .sqrt_k .safe_sub(market.amm.sqrt_k.safe_div(1000)?)? - .max(market.amm.user_lp_shares.safe_add(1)?); + .max(new_sqrt_k_lower_bound); let update_k_result = cp_curve::get_update_k_result(market, bn::U192::from(new_sqrt_k), true)?; diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index c3c3bf5b1..d8f6f9f85 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -7,7 +7,9 @@ use anchor_lang::{prelude::*, solana_program::msg}; #[derive(Clone, Copy, Debug)] pub enum MarginCalculationMode { - Standard, + Standard { + track_open_orders_fraction: bool, + }, Liquidation { margin_buffer: u128, market_to_track_margin_requirement: Option, @@ -47,7 +49,9 @@ impl MarginContext { pub fn standard(margin_type: MarginRequirementType) -> Self { Self { margin_type, - mode: MarginCalculationMode::Standard, + mode: MarginCalculationMode::Standard { + track_open_orders_fraction: false, + }, strict: false, } } @@ -57,6 +61,21 @@ 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 liquidation(margin_buffer: u32) -> Self { Self { margin_type: MarginRequirementType::Maintenance, @@ -104,6 +123,7 @@ pub struct MarginCalculation { pub total_spot_asset_value: i128, pub total_spot_liability_value: u128, pub total_perp_liability_value: u128, + pub open_orders_margin_requirement: u128, tracked_market_margin_requirement: u128, } @@ -121,6 +141,7 @@ impl MarginCalculation { total_spot_asset_value: 0, total_spot_liability_value: 0, total_perp_liability_value: 0, + open_orders_margin_requirement: 0, tracked_market_margin_requirement: 0, } } @@ -137,6 +158,7 @@ impl MarginCalculation { market_identifier: MarketIdentifier, ) -> DriftResult { self.margin_requirement = self.margin_requirement.safe_add(margin_requirement)?; + if let MarginCalculationMode::Liquidation { margin_buffer, .. } = self.context.mode { self.margin_requirement_plus_buffer = self .margin_requirement_plus_buffer @@ -156,6 +178,13 @@ 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(()) @@ -195,6 +224,14 @@ impl MarginCalculation { self.total_collateral >= self.margin_requirement as i128 } + pub fn positions_meets_margin_requirement(&self) -> DriftResult { + Ok(self.total_collateral + >= self + .margin_requirement + .safe_sub(self.open_orders_margin_requirement)? + .cast::()?) + } + pub fn can_exit_liquidation(&self) -> DriftResult { if !self.is_liquidation_mode() { msg!("liquidation mode not enabled"); @@ -254,4 +291,13 @@ impl MarginCalculation { fn is_liquidation_mode(&self) -> bool { 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 + } + ) + } } diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index 3e4eb47d0..c62c62db2 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -7,6 +7,7 @@ pub mod margin_calculation; pub mod oracle; pub mod oracle_map; pub mod order_params; +pub mod paused_operations; pub mod perp_market; pub mod perp_market_map; pub mod spot_fulfillment_params; diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index f36045404..132d41b9e 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -92,6 +92,66 @@ impl OrderParams { Ok(()) } + + pub fn get_close_perp_params( + market: &PerpMarket, + direction_to_close: PositionDirection, + base_asset_amount: u64, + ) -> DriftResult { + let auction_start_price = market + .amm + .last_ask_price_twap + .safe_add(market.amm.last_bid_price_twap)? + .safe_div(2)? + .cast::()? + .safe_sub(market.amm.historical_oracle_data.last_oracle_price_twap)?; + + let oracle_twap_u64 = market + .amm + .historical_oracle_data + .last_oracle_price_twap + .unsigned_abs(); + let auction_end_price_buffer = market + .amm + .mark_std + .max(market.amm.oracle_std) + .clamp(oracle_twap_u64 / 100, oracle_twap_u64 / 20); + + let auction_end_price = if direction_to_close == PositionDirection::Short { + let auction_end_price = market + .amm + .last_bid_price_twap + .safe_sub(auction_end_price_buffer)? + .cast::()? + .safe_sub(market.amm.historical_oracle_data.last_oracle_price_twap)?; + auction_end_price.min(auction_start_price) + } else { + let auction_end_price = market + .amm + .last_ask_price_twap + .safe_add(auction_end_price_buffer)? + .cast::()? + .safe_sub(market.amm.historical_oracle_data.last_oracle_price_twap)?; + + auction_end_price.max(auction_start_price) + }; + + let params = OrderParams { + market_type: MarketType::Perp, + direction: direction_to_close, + order_type: OrderType::Oracle, + market_index: market.market_index, + base_asset_amount, + reduce_only: true, + auction_start_price: Some(auction_start_price), + auction_end_price: Some(auction_end_price), + auction_duration: Some(80), + oracle_price_offset: Some(auction_end_price.cast()?), + ..OrderParams::default() + }; + + Ok(params) + } } fn get_auction_duration(price_diff: u64, price: u64) -> DriftResult { diff --git a/programs/drift/src/state/order_params/tests.rs b/programs/drift/src/state/order_params/tests.rs index b349972d4..894b43f3c 100644 --- a/programs/drift/src/state/order_params/tests.rs +++ b/programs/drift/src/state/order_params/tests.rs @@ -221,3 +221,326 @@ mod update_perp_auction_params { ); } } + +mod get_close_perp_params { + use crate::state::oracle::HistoricalOracleData; + use crate::state::order_params::PostOnlyParam; + use crate::state::perp_market::{PerpMarket, AMM}; + use crate::state::user::{Order, OrderStatus}; + use crate::test_utils::create_account_info; + use crate::validation::order::validate_order; + use crate::{ + OrderParams, PositionDirection, BASE_PRECISION_U64, PRICE_PRECISION_I64, + PRICE_PRECISION_U64, + }; + use anchor_lang::prelude::AccountLoader; + use solana_program::pubkey::Pubkey; + use std::str::FromStr; + + #[test] + fn bid() { + let oracle_price = 100 * PRICE_PRECISION_I64; + let slot = 1; + let amm = AMM { + last_ask_price_twap: 101 * PRICE_PRECISION_U64, + last_bid_price_twap: 99 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + mark_std: PRICE_PRECISION_U64, + oracle_std: PRICE_PRECISION_U64, + ..AMM::default_test() + }; + let perp_market = PerpMarket { + amm, + ..PerpMarket::default() + }; + + let direction_to_close = PositionDirection::Long; + let base_asset_amount = BASE_PRECISION_U64; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, 0); + assert_eq!(auction_end_price, 2 * PRICE_PRECISION_I64); + assert_eq!(oracle_price_offset, 2 * PRICE_PRECISION_I64 as i32); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + + let amm = AMM { + last_ask_price_twap: 103 * PRICE_PRECISION_U64, + last_bid_price_twap: 101 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + mark_std: PRICE_PRECISION_U64, + oracle_std: PRICE_PRECISION_U64, + ..AMM::default_test() + }; + let perp_market = PerpMarket { + amm, + ..PerpMarket::default() + }; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, 2 * PRICE_PRECISION_I64); + assert_eq!(auction_end_price, 4 * PRICE_PRECISION_I64); + assert_eq!(oracle_price_offset, 4 * PRICE_PRECISION_I64 as i32); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + + let amm = AMM { + last_ask_price_twap: 99 * PRICE_PRECISION_U64, + last_bid_price_twap: 97 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + mark_std: PRICE_PRECISION_U64, + oracle_std: PRICE_PRECISION_U64, + ..AMM::default_test() + }; + let perp_market = PerpMarket { + amm, + ..PerpMarket::default() + }; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, -2 * PRICE_PRECISION_I64); + assert_eq!(auction_end_price, 0); + assert_eq!(oracle_price_offset, 0); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + } + + #[test] + fn ask() { + let oracle_price = 100 * PRICE_PRECISION_I64; + let slot = 1; + let amm = AMM { + last_ask_price_twap: 101 * PRICE_PRECISION_U64, + last_bid_price_twap: 99 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + mark_std: PRICE_PRECISION_U64, + oracle_std: PRICE_PRECISION_U64, + ..AMM::default_test() + }; + let perp_market = PerpMarket { + amm, + ..PerpMarket::default() + }; + + let direction_to_close = PositionDirection::Short; + let base_asset_amount = BASE_PRECISION_U64; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, 0); + assert_eq!(auction_end_price, -2 * PRICE_PRECISION_I64); + assert_eq!(oracle_price_offset, -2 * PRICE_PRECISION_I64 as i32); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + + let amm = AMM { + last_ask_price_twap: 103 * PRICE_PRECISION_U64, + last_bid_price_twap: 101 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + mark_std: PRICE_PRECISION_U64, + oracle_std: PRICE_PRECISION_U64, + ..AMM::default_test() + }; + let perp_market = PerpMarket { + amm, + ..PerpMarket::default() + }; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, 2 * PRICE_PRECISION_I64); + assert_eq!(auction_end_price, 0); + assert_eq!(oracle_price_offset, 0); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + + let amm = AMM { + last_ask_price_twap: 99 * PRICE_PRECISION_U64, + last_bid_price_twap: 97 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + mark_std: PRICE_PRECISION_U64, + oracle_std: PRICE_PRECISION_U64, + ..AMM::default_test() + }; + let perp_market = PerpMarket { + amm, + ..PerpMarket::default() + }; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, -2 * PRICE_PRECISION_I64); + assert_eq!(auction_end_price, -4 * PRICE_PRECISION_I64); + assert_eq!(oracle_price_offset, -4 * PRICE_PRECISION_I64 as i32); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + } + + #[test] + fn btc() { + let perp_market_str = String::from("Ct8MLGv1N/cV6vWLwJY+18dY2GsrmrNldgnISB7pmbcf7cn9S4FZ4OYt9si0qF/hpn20TcEt5dszD3rGa3LcZYr+3w9KQVtDd3+9kQoAAAAAAAAAAAAAAAEAAAAAAAAA2VkiggoAAAC/dZSICgAAACeqnmUAAAAAeCbW5P///////////////8J7Hv4BAAAAAAAAAAAAAAB7+rQtykoAAAAAAAAAAAAAAAAAAAAAAABlO/erzgEAAAAAAAAAAAAAVnP4srYEAAAAAAAAAAAAAJxiDwAAAAAAAAAAAAAAAAAy7nN6ywEAAAAAAAAAAAAA5ihcH9MBAAAAAAAAAAAAAK7izzLrAgAAAAAAAAAAAADs3G4NBAAAAAAAAAAAAAAAYIhJGrUEAAAAAAAAAAAAAKA0JMEnAAAAAAAAAAAAAADg/mJJ2f//////////////aJbnnAAAAAAAAAAAAAAAABidn20AAAAAAAAAAAAAAAAARCk1OgAAAAAAAAAAAAAA/U3ihP3//////////////0p/wecT+f////////////8elGWXkwYAAAAAAAAAAAAAbccyGPz4/////////////+ZmycPDBgAAAAAAAAAAAAAASI58awAAAAAAAAAAAAAArC2A7gAAAACsLYDuAAAAAKwtgO4AAAAApwxIKwEAAABrEoqhLAAAAAAAAAAAAAAAf+nRyBMAAAAAAAAAAAAAAIagdCkZAAAAAAAAAAAAAADQH9cHJgAAAAAAAAAAAAAAc132XBgAAAAAAAAAAAAAAATX1A4SAAAAAAAAAAAAAADSZHePVgcAAAAAAAAAAAAA99MFdFYHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACE4MmozQEAAAAAAAAAAAAAHWZqWLkEAAAAAAAAAAAAACdJh8HOAQAAAAAAAAAAAADzKL56tgQAAAAAAAAAAAAAd3+9kQoAAAAAAAAAAAAAALJBWoMKAAAAJf9eiwoAAABroFyHCgAAAIv2go0KAAAAPT5dDgAAAAAEAgAAAAAAAAFRgdb/////MqOeZQAAAAAQDgAAAAAAAKCGAQAAAAAAoIYBAAAAAAAgoQcAAAAAAAAAAAAAAAAAscrx5+8FAACIP1dQJgAAAEGRyqEnAAAAJ6qeZQAAAABr7TAQAAAAAJ4lmw8AAAAAJ6qeZQAAAAAUAAAALEwAACARAABsAQAAKhoAAAAAAADcBTIAZMgAAYCLLeUAAAAAKHVdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFiluuwDJwEAAAAAAAAAAAAAAAAAAAAAAEJUQy1QRVJQICAgICAgICAgICAgICAgICAgICAgICAgWXIm/v////8AwusLAAAAAAB0O6QLAAAAvz8ZJAAAAACLqJ5lAAAAAADKmjsAAAAAAAAAAAAAAAAAAAAAAAAAAKcPDQAAAAAA8SQAAAAAAAC9AwAAAAAAAEAfAAAAAAAATB0AANQwAAD0AQAALAEAAAAAAAAQJwAApwUAABEJAAABAAEAAAAAALX/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 = perp_market.amm.historical_oracle_data.last_oracle_price; + let slot = 240991856_u64; + + let direction_to_close = PositionDirection::Short; + let base_asset_amount = BASE_PRECISION_U64; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, 87705234); + assert_eq!(auction_end_price, -430888573); + assert_eq!(oracle_price_offset, -430888573); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + } + + #[test] + fn doge() { + let perp_market_str = String::from("Ct8MLGv1N/cueW7q94VBpwLPordbGCeLrp/R8owsajNEG7L2nvhZ8NzvUN0KTNLcwX5F3xZ23LM2oRphxp33oCmbAVDGctJc8y4BAAAAAAAAAAAAAAAAAAEAAAAAAAAAiC8BAAAAAABMLwEAAAAAACmrnmUAAAAAmSxi7CT8/////////////zgrThgAAAAAAAAAAAAAAADdzXKMUwsAAAAAAAAAAAAAAAAAAAAAAADP1HhexhXAAgAAAAAAAAAAdDGk8Gq1xwIAAAAAAAAAAAzkDwAAAAAAAAAAAAAAAAAply2wnkelAgAAAAAAAAAA4qQewS2M3gIAAAAAAAAAAInzGxP44sMCAAAAAAAAAAC0KwEAAAAAAAAAAAAAAAAABtbOzyJzxgIAAAAAAAAAAACcfFCu/wYAAAAAAAAAAAAAnFHtB0b6////////////9GoMAGU/AQAAAAAAAAAAAAzNwT1RBgAAAAAAAAAAAAAAAMFv8oYjAAAAAAAAAAAABhCDPfz//////////////6bEBnzX//////////////95+qpnJAAAAAAAAAAAAAAAwQyrjdX//////////////33ohvUnAAAAAAAAAAAAAAAA/As7QZ0VAAAAAAAAAAAA8iQAAAAAAADyJAAAAAAAAPIkAAAAAAAA1wYAAAAAAABg33mwCgAAAAAAAAAAAAAABY12UwkAAAAAAAAAAAAAALwqCWEBAAAAAAAAAAAAAACQMZk6EwAAAAAAAAAAAAAAnzjKCwEAAAAAAAAAAAAAAApzcx8BAAAAAAAAAAAAAADLvbQBAAAAAAAAAAAAAAAAy720AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHhwa6WVC6AgAAAAAAAAAASl7rdy6XzQIAAAAAAAAAAPsDHiPPL8MCAAAAAAAAAACXfZpmTpbEAgAAAAAAAAAA8y4BAAAAAAD0/////////zIuAQAAAAAATTIBAAAAAAA/MAEAAAAAAGgwAQAAAAAAgEBdDgAAAAA3AgAAAAAAAGCTe/7/////66KeZQAAAAAQDgAAAAAAAACUNXcAAAAACgAAAAAAAAAAdDukCwAAAAAAAAAAAAAAc3fY9xsAAAD1rzWPAAAAABtgqEAAAAAAdKqeZQAAAAAlAAAAAAAAAJUAAAAAAAAAKaueZQAAAAAcJQAAgDgBAF1AAAAuIgAA1QEAAAAAAAD0ATIAZGQAAQAAAAAFAAAANbUVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADzUZfxOTwAAAAAAAAAAAAAAAAAAAAAAAERPR0UtUEVSUCAgICAgICAgICAgICAgICAgICAgICAg5Nyg//////+AlpgAAAAAAAAvaFkAAAAAMZviAQAAAABXpJ5lAAAAABAnAAAAAAAAAAAAAAAAAAAAAAAAAAAAABuUAAAAAAAAFRoAAAAAAAC+CgAAAAAAAMgAAADIAAAAECcAAKhhAADoAwAA9AEAAAAAAAAQJwAA2AAAAEkBAAAHAAEAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + 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 = perp_market.amm.historical_oracle_data.last_oracle_price; + let slot = 240991856_u64; + + let direction_to_close = PositionDirection::Short; + let base_asset_amount = 100 * BASE_PRECISION_U64; + + let params = + OrderParams::get_close_perp_params(&perp_market, direction_to_close, base_asset_amount) + .unwrap(); + + 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, 183); + assert_eq!(auction_end_price, -1119); + assert_eq!(oracle_price_offset, -1119); + + let order = get_order(¶ms, slot); + + validate_order(&order, &perp_market, Some(oracle_price), slot).unwrap(); + } + + fn get_order(params: &OrderParams, slot: u64) -> Order { + Order { + status: OrderStatus::Open, + order_type: params.order_type, + market_type: params.market_type, + slot, + order_id: 1, + user_order_id: params.user_order_id, + market_index: params.market_index, + price: params.price, + existing_position_direction: PositionDirection::Long, + base_asset_amount: params.base_asset_amount, + base_asset_amount_filled: 0, + quote_asset_amount_filled: 0, + direction: params.direction, + reduce_only: params.reduce_only, + trigger_price: params.trigger_price.unwrap_or(0), + trigger_condition: params.trigger_condition, + post_only: params.post_only != PostOnlyParam::None, + oracle_price_offset: params.oracle_price_offset.unwrap_or(0), + immediate_or_cancel: params.immediate_or_cancel, + auction_start_price: params.auction_start_price.unwrap_or(0), + auction_end_price: params.auction_end_price.unwrap_or(0), + auction_duration: params.auction_duration.unwrap_or(0), + max_ts: 100, + padding: [0; 3], + } + } +} diff --git a/programs/drift/src/state/paused_operations.rs b/programs/drift/src/state/paused_operations.rs new file mode 100644 index 000000000..e445439bb --- /dev/null +++ b/programs/drift/src/state/paused_operations.rs @@ -0,0 +1,62 @@ +use solana_program::msg; + +#[cfg(test)] +mod tests; + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum PerpOperation { + UpdateFunding = 0b00000001, + AmmFill = 0b00000010, + Fill = 0b00000100, + SettlePnl = 0b00001000, + SettlePnlWithPosition = 0b00010000, +} + +const ALL_PERP_OPERATIONS: [PerpOperation; 5] = [ + PerpOperation::UpdateFunding, + PerpOperation::AmmFill, + PerpOperation::Fill, + PerpOperation::SettlePnl, + PerpOperation::SettlePnlWithPosition, +]; + +impl PerpOperation { + pub fn is_operation_paused(current: u8, operation: PerpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_PERP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum SpotOperation { + UpdateCumulativeInterest = 0b00000001, + Fill = 0b00000010, + Withdraw = 0b00000100, +} + +const ALL_SPOT_OPERATIONS: [SpotOperation; 3] = [ + SpotOperation::UpdateCumulativeInterest, + SpotOperation::Fill, + SpotOperation::Withdraw, +]; + +impl SpotOperation { + pub fn is_operation_paused(current: u8, operation: SpotOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_SPOT_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} diff --git a/programs/drift/src/state/paused_operations/tests.rs b/programs/drift/src/state/paused_operations/tests.rs new file mode 100644 index 000000000..53461505a --- /dev/null +++ b/programs/drift/src/state/paused_operations/tests.rs @@ -0,0 +1,79 @@ +mod test { + use crate::state::paused_operations::PerpOperation; + + #[test] + fn test_is_operation_paused() { + // Test each variant individually + assert!(PerpOperation::is_operation_paused( + 0b00000001, + PerpOperation::UpdateFunding + )); + assert!(PerpOperation::is_operation_paused( + 0b00000010, + PerpOperation::AmmFill + )); + assert!(PerpOperation::is_operation_paused( + 0b00000100, + PerpOperation::Fill + )); + assert!(PerpOperation::is_operation_paused( + 0b00001000, + PerpOperation::SettlePnl + )); + + // Test combinations + let all_operations = PerpOperation::UpdateFunding as u8 + | PerpOperation::AmmFill as u8 + | PerpOperation::Fill as u8 + | PerpOperation::SettlePnl as u8; + assert!(PerpOperation::is_operation_paused( + all_operations, + PerpOperation::UpdateFunding + )); + assert!(PerpOperation::is_operation_paused( + all_operations, + PerpOperation::AmmFill + )); + assert!(PerpOperation::is_operation_paused( + all_operations, + PerpOperation::Fill + )); + assert!(PerpOperation::is_operation_paused( + all_operations, + PerpOperation::SettlePnl + )); + + let no_operations = 0; + assert!(!PerpOperation::is_operation_paused( + no_operations, + PerpOperation::UpdateFunding + )); + assert!(!PerpOperation::is_operation_paused( + no_operations, + PerpOperation::AmmFill + )); + assert!(!PerpOperation::is_operation_paused( + no_operations, + PerpOperation::Fill + )); + assert!(!PerpOperation::is_operation_paused( + no_operations, + PerpOperation::SettlePnl + )); + + // Test with multiple operations + let multiple_operations = PerpOperation::AmmFill as u8 | PerpOperation::SettlePnl as u8; + assert!(PerpOperation::is_operation_paused( + multiple_operations as u8, + PerpOperation::AmmFill + )); + assert!(PerpOperation::is_operation_paused( + multiple_operations as u8, + PerpOperation::SettlePnl + )); + assert!(!PerpOperation::is_operation_paused( + multiple_operations as u8, + PerpOperation::Fill + )); + } +} diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 862a7ee23..06dc486fc 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -11,9 +11,9 @@ use crate::math::constants::{ AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64, }; use crate::math::constants::{ - AMM_RESERVE_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, LP_FEE_SLICE_DENOMINATOR, - LP_FEE_SLICE_NUMERATOR, MARGIN_PRECISION_U128, PERCENTAGE_PRECISION, SPOT_WEIGHT_PRECISION, - TWENTY_FOUR_HOUR, + AMM_RESERVE_PRECISION_I128, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_U128, + LP_FEE_SLICE_DENOMINATOR, LP_FEE_SLICE_NUMERATOR, MARGIN_PRECISION_U128, PERCENTAGE_PRECISION, + SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; use crate::math::helpers::get_proportion_i128; @@ -31,6 +31,7 @@ use crate::state::traits::{MarketIndexOffset, Size}; use crate::{AMM_TO_QUOTE_PRECISION_RATIO, PRICE_PRECISION}; use borsh::{BorshDeserialize, BorshSerialize}; +use crate::state::paused_operations::PerpOperation; use drift_macros::assert_no_slop; use static_assertions::const_assert_eq; @@ -43,13 +44,13 @@ pub enum MarketStatus { Initialized, /// all operations allowed Active, - /// funding rate updates are paused + /// Deprecated in favor of PausedOperations FundingPaused, - /// amm fills are prevented/blocked + /// Deprecated in favor of PausedOperations AmmPaused, - /// fills are blocked + /// Deprecated in favor of PausedOperations FillPaused, - /// perp: pause settling negative pnl | spot: pause depositing asset + /// Deprecated in favor of PausedOperations WithdrawPaused, /// fills only able to reduce liability ReduceOnly, @@ -65,6 +66,23 @@ impl Default for MarketStatus { } } +impl MarketStatus { + pub fn validate_not_deprecated(&self) -> DriftResult { + if matches!( + self, + MarketStatus::FundingPaused + | MarketStatus::AmmPaused + | MarketStatus::FillPaused + | MarketStatus::WithdrawPaused + ) { + msg!("MarketStatus is deprecated"); + Err(ErrorCode::DefaultError) + } else { + Ok(()) + } + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum ContractType { Perpetual, @@ -202,7 +220,7 @@ pub struct PerpMarket { /// The contract tier determines how much insurance a market can receive, with more speculative markets receiving less insurance /// It also influences the order perp markets can be liquidated, with less speculative markets being liquidated first pub contract_tier: ContractTier, - pub padding1: u8, + pub paused_operations: u8, /// The spot market that pnl is settled in pub quote_spot_market_index: u16, /// Between -100 and 100, represents what % to increase/decrease the fee by @@ -240,7 +258,7 @@ impl Default for PerpMarket { status: MarketStatus::default(), contract_type: ContractType::default(), contract_tier: ContractTier::default(), - padding1: 0, + paused_operations: 0, quote_spot_market_index: 0, fee_adjustment: 0, padding: [0; 46], @@ -257,19 +275,23 @@ impl MarketIndexOffset for PerpMarket { } impl PerpMarket { - pub fn is_active(&self, now: i64) -> DriftResult { - let status_ok = !matches!( + pub fn is_in_settlement(&self, now: i64) -> bool { + let in_settlement = matches!( self.status, MarketStatus::Settlement | MarketStatus::Delisted ); - let not_expired = self.expiry_ts == 0 || now < self.expiry_ts; - Ok(status_ok && not_expired) + let expired = self.expiry_ts != 0 && now >= self.expiry_ts; + in_settlement || expired } pub fn is_reduce_only(&self) -> DriftResult { Ok(self.status == MarketStatus::ReduceOnly) } + pub fn is_operation_paused(&self, operation: PerpOperation) -> bool { + PerpOperation::is_operation_paused(self.paused_operations, operation) + } + pub fn get_sanitize_clamp_denominator(self) -> DriftResult> { Ok(match self.contract_tier { ContractTier::A => Some(10_i64), // 10% @@ -826,6 +848,15 @@ 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()?), + )) + } + pub fn get_protocol_owned_position(self) -> DriftResult { self.base_asset_amount_with_amm .safe_add(self.base_asset_amount_with_unsettled_lp)? @@ -1134,6 +1165,35 @@ impl AMM { Ok(()) } + + pub fn get_new_oracle_conf_pct( + &self, + confidence: u64, // price precision + reserve_price: u64, // price precision + now: i64, + ) -> DriftResult { + // use previous value decayed as lower bound to avoid shrinking too quickly + let upper_bound_divisor = 21_u64; + let lower_bound_divisor = 5_u64; + let since_last = now + .safe_sub(self.historical_oracle_data.last_oracle_price_twap_ts)? + .max(0); + + let confidence_lower_bound = if since_last > 0 { + let confidence_divisor = upper_bound_divisor + .saturating_sub(since_last.cast::()?) + .max(lower_bound_divisor); + self.last_oracle_conf_pct + .safe_sub(self.last_oracle_conf_pct / confidence_divisor)? + } else { + self.last_oracle_conf_pct + }; + + Ok(confidence + .safe_mul(BID_ASK_SPREAD_PRECISION)? + .safe_div(reserve_price)? + .max(confidence_lower_bound)) + } } #[cfg(test)] diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 3f13623be..cce76907c 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -17,6 +17,7 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{calculate_utilization, get_token_amount, get_token_value}; use crate::state::oracle::{HistoricalIndexData, HistoricalOracleData, OracleSource}; +use crate::state::paused_operations::SpotOperation; use crate::state::perp_market::{MarketStatus, PoolBalance}; use crate::state::traits::{MarketIndexOffset, Size}; use crate::validate; @@ -156,7 +157,8 @@ pub struct SpotMarket { pub status: MarketStatus, /// The asset tier affects how a deposit can be used as collateral and the priority for a borrow being liquidated pub asset_tier: AssetTier, - pub padding1: [u8; 6], + pub paused_operations: u8, + pub padding1: [u8; 5], /// For swaps, the amount of token loaned out in the begin_swap ix /// precision: token mint precision pub flash_loan_amount: u64, @@ -224,7 +226,8 @@ impl Default for SpotMarket { oracle_source: OracleSource::default(), status: MarketStatus::default(), asset_tier: AssetTier::default(), - padding1: [0; 6], + paused_operations: 0, + padding1: [0; 5], flash_loan_amount: 0, flash_loan_initial_token_amount: 0, total_swap_fee: 0, @@ -243,27 +246,26 @@ impl MarketIndexOffset for SpotMarket { } impl SpotMarket { - pub fn is_active(&self, now: i64) -> DriftResult { - let status_ok = !matches!( + pub fn is_in_settlement(&self, now: i64) -> bool { + let in_settlement = matches!( self.status, MarketStatus::Settlement | MarketStatus::Delisted ); - let not_expired = self.expiry_ts == 0 || now < self.expiry_ts; - Ok(status_ok && not_expired) + let expired = self.expiry_ts != 0 && now >= self.expiry_ts; + in_settlement || expired } pub fn is_reduce_only(&self) -> bool { self.status == MarketStatus::ReduceOnly } + pub fn is_operation_paused(&self, operation: SpotOperation) -> bool { + SpotOperation::is_operation_paused(self.paused_operations, operation) + } + pub fn fills_enabled(&self) -> bool { - matches!( - self.status, - MarketStatus::Active - | MarketStatus::FundingPaused - | MarketStatus::ReduceOnly - | MarketStatus::WithdrawPaused - ) + matches!(self.status, MarketStatus::Active | MarketStatus::ReduceOnly) + && !self.is_operation_paused(SpotOperation::Fill) } pub fn get_sanitize_clamp_denominator(&self) -> DriftResult> { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 7c8f9c0af..0bf2aa6ac 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -919,6 +919,14 @@ impl PerpPosition { .map(|delta| delta.max(0))? .safe_add(pnl_pool_excess.max(0))?; + if max_positive_pnl < unrealized_pnl { + msg!( + "Claimable pnl below position upnl: {} < {}", + max_positive_pnl, + unrealized_pnl + ); + } + Ok(unrealized_pnl.min(max_positive_pnl)) } else { Ok(unrealized_pnl) diff --git a/programs/drift/src/validation/order.rs b/programs/drift/src/validation/order.rs index 3661c1ed7..fe8f1fa18 100644 --- a/programs/drift/src/validation/order.rs +++ b/programs/drift/src/validation/order.rs @@ -220,6 +220,11 @@ fn validate_post_only_order( valid_oracle_price: Option, slot: u64, ) -> DriftResult { + // jit maker can fill against amm + if order.is_jit_maker() { + return Ok(()); + } + let limit_price = order.force_get_limit_price(valid_oracle_price, None, slot, market.amm.order_tick_size)?; diff --git a/sdk/VERSION b/sdk/VERSION index d523d4af5..5b6ad2738 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.54.0-beta.4 \ No newline at end of file +2.60.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index f2e71a433..383c11407 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.54.0-beta.4", + "version": "2.60.0-beta.0", "main": "lib/index.js", "types": "lib/index.d.ts", "author": "crispheaney", diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index 440148927..04cc08a5a 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -43,6 +43,10 @@ export interface DriftClientAccountEvents { error: (e: Error) => void; } +export interface DriftClientMetricsEvents { + txSigned: void; +} + export interface DriftClientAccountSubscriber { eventEmitter: StrictEventEmitter; isSubscribed: boolean; diff --git a/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts b/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts index ca6bb2aff..3106cbc89 100644 --- a/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts +++ b/sdk/src/accounts/webSocketDriftClientAccountSubscriber.ts @@ -104,7 +104,7 @@ export class WebSocketDriftClientAccountSubscriber this.program, statePublicKey, undefined, - this.resubTimeoutMs, + undefined, this.commitment ); await this.stateAccountSubscriber.subscribe((data: StateAccount) => { diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 2170ffbf7..001dd8097 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -358,6 +358,32 @@ export class AdminClient extends DriftClient { return txSig; } + public async recenterPerpMarketAmm( + perpMarketIndex: number, + pegMultiplier: BN, + sqrtK: BN + ): Promise { + const marketPublicKey = await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ); + + const tx = await this.program.transaction.recenterPerpMarketAmm( + pegMultiplier, + sqrtK, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + perpMarket: marketPublicKey, + }, + } + ); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + public async updatePerpMarketConcentrationScale( perpMarketIndex: number, concentrationScale: BN @@ -1335,6 +1361,28 @@ export class AdminClient extends DriftClient { return txSig; } + public async updateSpotMarketPausedOperations( + spotMarketIndex: number, + pausedOperations: number + ): Promise { + const tx = await this.program.transaction.updateSpotMarketPausedOperations( + pausedOperations, + { + accounts: { + admin: this.wallet.publicKey, + state: await this.getStatePublicKey(), + spotMarket: await getSpotMarketPublicKey( + this.program.programId, + spotMarketIndex + ), + }, + } + ); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + public async updatePerpMarketStatus( perpMarketIndex: number, marketStatus: MarketStatus @@ -1357,6 +1405,28 @@ export class AdminClient extends DriftClient { return txSig; } + public async updatePerpMarketPausedOperations( + perpMarketIndex: number, + pausedOperations: number + ): Promise { + const tx = await this.program.transaction.updatePerpMarketPausedOperations( + pausedOperations, + { + accounts: { + admin: this.wallet.publicKey, + state: await this.getStatePublicKey(), + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + }, + } + ); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + public async updatePerpMarketContractTier( perpMarketIndex: number, contractTier: ContractTier diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 7bfc2a8cd..96d32985d 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -244,6 +244,16 @@ export const DevnetPerpMarkets: PerpMarketConfig[] = [ launchTs: 1704209558000, oracleSource: OracleSource.PYTH, }, + // { + // fullName: 'WIF', + // category: ['Meme', 'Dog'], + // symbol: 'WIF-PERP', + // baseAssetSymbol: 'WIF', + // marketIndex: 23, + // oracle: new PublicKey('5i1sz2QQjCQt9PnhuPvqbiYUAYCgjdRnza1JbiH2qRCo'), + // launchTs: 1706219971000, + // oracleSource: OracleSource.PYTH, + // }, ]; export const MainnetPerpMarkets: PerpMarketConfig[] = [ @@ -477,6 +487,16 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ launchTs: 1704209558000, oracleSource: OracleSource.PYTH, }, + { + fullName: 'WIF', + category: ['Meme', 'Dog'], + symbol: 'WIF-PERP', + baseAssetSymbol: 'WIF', + marketIndex: 23, + oracle: new PublicKey('6ABgrEZk8urs6kJ1JNdC1sspH5zKXRqxy8sg3ZG2cQps'), + launchTs: 1706219971000, + oracleSource: OracleSource.PYTH, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 94fed357e..f34e6d995 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -170,6 +170,16 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [ precisionExp: NINE, serumMarket: new PublicKey('H87FfmHABiZLRGrDsXRZtqq25YpARzaokCzL1vMYGiep'), }, + { + symbol: 'WIF', + marketIndex: 10, + oracle: new PublicKey('6ABgrEZk8urs6kJ1JNdC1sspH5zKXRqxy8sg3ZG2cQps'), + oracleSource: OracleSource.PYTH, + mint: new PublicKey('EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + serumMarket: new PublicKey('2BtDHBTCTUxvdur498ZEcMgimasaFrY5GzLv8wS8XgCb'), + }, ]; export const SpotMarkets: { [key in DriftEnv]: SpotMarketConfig[] } = { diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index f086d7a76..47a3f40ca 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -86,9 +86,10 @@ import { DriftClientAccountSubscriber, DriftClientAccountEvents, DataAndSlot, + DriftClientMetricsEvents, } from './accounts/types'; -import { TxSender, TxSigAndSlot } from './tx/types'; -import { wrapInTx } from './tx/utils'; +import { ExtraConfirmationOptions, TxSender, TxSigAndSlot } from './tx/types'; +import { getSignedTransactionMap, wrapInTx } from './tx/utils'; import { BASE_PRECISION, PRICE_PRECISION, @@ -150,6 +151,10 @@ export class DriftClient { userStatsAccountSubscriptionConfig: UserStatsSubscriptionConfig; accountSubscriber: DriftClientAccountSubscriber; eventEmitter: StrictEventEmitter; + metricsEventEmitter: StrictEventEmitter< + EventEmitter, + DriftClientMetricsEvents + >; _isSubscribed = false; txSender: TxSender; perpMarketLastSlotCache = new Map(); @@ -164,6 +169,7 @@ export class DriftClient { skipLoadUsers?: boolean; txVersion: TransactionVersion; txParams: TxParams; + enableMetricsEvents?: boolean; public get isSubscribed() { return this._isSubscribed && this.accountSubscriber.isSubscribed; @@ -288,6 +294,12 @@ export class DriftClient { ); } this.eventEmitter = this.accountSubscriber.eventEmitter; + + if (config.enableMetricsEvents) { + this.enableMetricsEvents = true; + this.metricsEventEmitter = new EventEmitter(); + } + this.txSender = config.txSender ?? new RetryTxSender({ @@ -892,7 +904,8 @@ export class DriftClient { } public async updateUserCustomMarginRatio( - updates: { marginRatio: number; subAccountId: number }[] + updates: { marginRatio: number; subAccountId: number }[], + txParams?: TxParams ): Promise { const ixs = await Promise.all( updates.map(async ({ marginRatio, subAccountId }) => { @@ -904,7 +917,7 @@ export class DriftClient { }) ); - const tx = await this.buildTransaction(ixs, this.txParams); + const tx = await this.buildTransaction(ixs, txParams ?? this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; @@ -1865,7 +1878,8 @@ export class DriftClient { fromSubAccountId?: number, referrerInfo?: ReferrerInfo, donateAmount?: BN, - txParams?: TxParams + txParams?: TxParams, + customMaxMarginRatio?: number ): Promise<[TransactionSignature, PublicKey]> { const ixs = []; @@ -1959,6 +1973,15 @@ export class DriftClient { ixs.push(donateIx); } + // Set the max margin ratio to initialize account with if passed + if (customMaxMarginRatio) { + const customMarginRatioIx = await this.getUpdateUserCustomMarginRatioIx( + customMaxMarginRatio, + subAccountId + ); + ixs.push(customMarginRatioIx); + } + // Close the wrapped sol account at the end of the transaction if (createWSOLTokenAccount) { ixs.push( @@ -2565,11 +2588,13 @@ export class DriftClient { txParams?: TxParams, bracketOrdersParams = new Array(), referrerInfo?: ReferrerInfo, - cancelExistingOrders?: boolean + cancelExistingOrders?: boolean, + settlePnl?: boolean ): Promise<{ txSig: TransactionSignature; signedFillTx?: Transaction; signedCancelExistingOrdersTx?: Transaction; + signedSettlePnlTx?: Transaction; }> { const marketIndex = orderParams.marketIndex; const orderId = userAccount.nextOrderId; @@ -2579,10 +2604,10 @@ export class DriftClient { userAccount.subAccountId ); - let cancelOrdersIx: TransactionInstruction; - let cancelExistingOrdersTx: Transaction; + /* Cancel open orders in market if requested */ + let cancelExistingOrdersTx; if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { - cancelOrdersIx = await this.getCancelOrdersIx( + const cancelOrdersIx = await this.getCancelOrdersIx( orderParams.marketType, orderParams.marketIndex, null, @@ -2597,6 +2622,23 @@ export class DriftClient { ); } + /* Settle PnL after fill if requested */ + let settlePnlTx; + if (settlePnl && isVariant(orderParams.marketType, 'perp')) { + const settlePnlIx = await this.settlePNLIx( + userAccountPublicKey, + userAccount, + marketIndex + ); + + //@ts-ignore + settlePnlTx = await this.buildTransaction( + [settlePnlIx], + txParams, + this.txVersion + ); + } + // use versioned transactions if there is a lookup table account and wallet is compatible if (this.txVersion === 0) { const versionedMarketOrderTx = await this.buildTransaction( @@ -2623,20 +2665,36 @@ export class DriftClient { 0 ); - const [ + const allPossibleTxs = [ + versionedMarketOrderTx, + versionedFillTx, + cancelExistingOrdersTx, + settlePnlTx, + ]; + const txKeys = [ + 'signedVersionedMarketOrderTx', + 'signedVersionedFillTx', + 'signedCancelExistingOrdersTx', + 'signedSettlePnlTx', + ]; + + const { signedVersionedMarketOrderTx, signedVersionedFillTx, signedCancelExistingOrdersTx, - ] = await this.provider.wallet.signAllTransactions( - [ - versionedMarketOrderTx, - versionedFillTx, - cancelExistingOrdersTx, - ].filter((tx) => tx !== undefined) + signedSettlePnlTx, + } = await getSignedTransactionMap( + //@ts-ignore + this.provider.wallet, + allPossibleTxs, + txKeys ); - const { txSig, slot } = await this.txSender.sendRawTransaction( - signedVersionedMarketOrderTx.serialize(), - this.opts + + const { txSig, slot } = await this.sendTransaction( + signedVersionedMarketOrderTx, + [], + this.opts, + true ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); @@ -2646,6 +2704,8 @@ export class DriftClient { signedFillTx: signedVersionedFillTx, // @ts-ignore signedCancelExistingOrdersTx, + // @ts-ignore + signedSettlePnlTx, }; } else { const marketOrderTx = wrapInTx( @@ -2667,12 +2727,33 @@ export class DriftClient { cancelExistingOrdersTx.feePayer = userAccount.authority; } - const [signedMarketOrderTx, signedCancelExistingOrdersTx] = - await this.provider.wallet.signAllTransactions( - [marketOrderTx, cancelExistingOrdersTx].filter( - (tx) => tx !== undefined - ) - ); + if (settlePnlTx) { + settlePnlTx.recentBlockhash = currentBlockHash; + settlePnlTx.feePayer = userAccount.authority; + } + + const allPossibleTxs = [ + marketOrderTx, + cancelExistingOrdersTx, + settlePnlTx, + ]; + const txKeys = [ + 'signedMarketOrderTx', + 'signedCancelExistingOrdersTx', + 'signedSettlePnlTx', + ]; + + const { + signedMarketOrderTx, + signedCancelExistingOrdersTx, + signedSettlePnlTx, + } = await getSignedTransactionMap( + //@ts-ignore + this.provider.wallet, + allPossibleTxs, + txKeys + ); + const { txSig, slot } = await this.sendTransaction( signedMarketOrderTx, [], @@ -2681,7 +2762,14 @@ export class DriftClient { ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); - return { txSig, signedFillTx: undefined, signedCancelExistingOrdersTx }; + return { + txSig, + signedFillTx: undefined, + //@ts-ignore + signedCancelExistingOrdersTx, + //@ts-ignore + signedSettlePnlTx, + }; } } @@ -4330,13 +4418,14 @@ export class DriftClient { bracketOrdersParams = new Array(), txParams?: TxParams, subAccountId?: number, - cancelExistingOrders?: boolean + cancelExistingOrders?: boolean, + settlePnl?: boolean ): Promise<{ txSig: TransactionSignature; signedCancelExistingOrdersTx?: Transaction; + signedSettlePnlTx?: Transaction; }> { - let signedCancelExistingOrdersTx: Transaction; - + let cancelExistingOrdersTx: Transaction; if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { const cancelOrdersIx = await this.getCancelOrdersIx( orderParams.marketType, @@ -4345,15 +4434,32 @@ export class DriftClient { subAccountId ); - const cancelExistingOrdersTx = await this.buildTransaction( + //@ts-ignore + cancelExistingOrdersTx = await this.buildTransaction( [cancelOrdersIx], txParams, this.txVersion ); + } - // @ts-ignore - signedCancelExistingOrdersTx = await this.provider.wallet.signTransaction( - cancelExistingOrdersTx + /* Settle PnL after fill if requested */ + let settlePnlTx: Transaction; + if (settlePnl && isVariant(orderParams.marketType, 'perp')) { + const userAccountPublicKey = await this.getUserAccountPublicKey( + subAccountId + ); + + const settlePnlIx = await this.settlePNLIx( + userAccountPublicKey, + this.getUserAccount(subAccountId), + orderParams.marketIndex + ); + + //@ts-ignore + settlePnlTx = await this.buildTransaction( + [settlePnlIx], + txParams, + this.txVersion ); } @@ -4376,14 +4482,40 @@ export class DriftClient { ixs.push(bracketOrdersIx); } + const placeAndTakeTx = await this.buildTransaction(ixs, txParams); + + const allPossibleTxs = [ + placeAndTakeTx, + cancelExistingOrdersTx, + settlePnlTx, + ]; + const txKeys = [ + 'signedPlaceAndTakeTx', + 'signedCancelExistingOrdersTx', + 'signedSettlePnlTx', + ]; + + const { + signedPlaceAndTakeTx, + signedCancelExistingOrdersTx, + signedSettlePnlTx, + } = await getSignedTransactionMap( + //@ts-ignore + this.provider.wallet, + allPossibleTxs, + txKeys + ); + const { txSig, slot } = await this.sendTransaction( - await this.buildTransaction(ixs, txParams), + signedPlaceAndTakeTx, [], - this.opts + this.opts, + true ); this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); - return { txSig, signedCancelExistingOrdersTx }; + //@ts-ignore + return { txSig, signedCancelExistingOrdersTx, signedSettlePnlTx }; } public async getPlaceAndTakePerpOrderIx( @@ -6215,25 +6347,38 @@ export class DriftClient { return undefined; } + private handleSignedTransaction() { + this.metricsEventEmitter.emit('txSigned'); + } + sendTransaction( tx: Transaction | VersionedTransaction, additionalSigners?: Array, opts?: ConfirmOptions, preSigned?: boolean ): Promise { + const extraConfirmationOptions: ExtraConfirmationOptions = this + .enableMetricsEvents + ? { + onSignedCb: this.handleSignedTransaction.bind(this), + } + : undefined; + if (tx instanceof VersionedTransaction) { return this.txSender.sendVersionedTransaction( tx as VersionedTransaction, additionalSigners, opts, - preSigned + preSigned, + extraConfirmationOptions ); } else { return this.txSender.send( tx as Transaction, additionalSigners, opts, - preSigned + preSigned, + extraConfirmationOptions ); } } diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index c8f3c614f..0c1809c35 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -32,6 +32,7 @@ export type DriftClientConfig = { skipLoadUsers?: boolean; // if passed to constructor, no user accounts will be loaded. they will load if updateWallet is called afterwards. txVersion?: TransactionVersion; // which tx version to use txParams?: TxParams; // default tx params to use + enableMetricsEvents?: boolean; }; export type DriftClientSubscriptionConfig = diff --git a/sdk/src/events/webSocketLogProvider.ts b/sdk/src/events/webSocketLogProvider.ts index 4d82f89bd..9fb896f65 100644 --- a/sdk/src/events/webSocketLogProvider.ts +++ b/sdk/src/events/webSocketLogProvider.ts @@ -1,5 +1,11 @@ import { LogProvider, logProviderCallback } from './types'; -import { Commitment, Connection, PublicKey } from '@solana/web3.js'; +import { + Commitment, + Connection, + Context, + Logs, + PublicKey, +} from '@solana/web3.js'; import { EventEmitter } from 'events'; export class WebSocketLogProvider implements LogProvider { @@ -45,7 +51,7 @@ export class WebSocketLogProvider implements LogProvider { public setSubscription(callback: logProviderCallback): void { this.subscriptionId = this.connection.onLogs( this.address, - (logs, ctx) => { + (logs: Logs, ctx: Context) => { if (this.resubTimeoutMs && !this.isUnsubscribing) { this.receivingData = true; clearTimeout(this.timeoutId); @@ -55,6 +61,9 @@ export class WebSocketLogProvider implements LogProvider { } this.reconnectAttempts = 0; } + if (logs.err !== null) { + return; + } callback(logs.signature, ctx.slot, logs.logs, undefined); }, this.commitment diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 848d5d18a..86a524a54 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.53.0", + "version": "2.59.0", "name": "drift", "instructions": [ { @@ -2972,6 +2972,36 @@ } ] }, + { + "name": "recenterPerpMarketAmm", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pegMultiplier", + "type": "u128" + }, + { + "name": "sqrtK", + "type": "u128" + } + ] + }, { "name": "updatePerpMarketExpiry", "accounts": [ @@ -3494,6 +3524,32 @@ } ] }, + { + "name": "updateSpotMarketPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, { "name": "updateSpotMarketAssetTier", "accounts": [ @@ -3828,6 +3884,32 @@ } ] }, + { + "name": "updatePerpMarketPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, { "name": "updatePerpMarketContractTier", "accounts": [ @@ -5228,7 +5310,7 @@ } }, { - "name": "padding1", + "name": "pausedOperations", "type": "u8" }, { @@ -5641,12 +5723,16 @@ "defined": "AssetTier" } }, + { + "name": "pausedOperations", + "type": "u8" + }, { "name": "padding1", "type": { "array": [ "u8", - 6 + 5 ] } }, @@ -8524,7 +8610,13 @@ "kind": "enum", "variants": [ { - "name": "Standard" + "name": "Standard", + "fields": [ + { + "name": "track_open_orders_fraction", + "type": "bool" + } + ] }, { "name": "Liquidation", @@ -8606,6 +8698,46 @@ ] } }, + { + "name": "PerpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateFunding" + }, + { + "name": "AmmFill" + }, + { + "name": "Fill" + }, + { + "name": "SettlePnl" + }, + { + "name": "SettlePnlWithPosition" + } + ] + } + }, + { + "name": "SpotOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UpdateCumulativeInterest" + }, + { + "name": "Fill" + }, + { + "name": "Withdraw" + } + ] + } + }, { "name": "MarketStatus", "type": { diff --git a/sdk/src/math/exchangeStatus.ts b/sdk/src/math/exchangeStatus.ts index d0789f519..716de6b63 100644 --- a/sdk/src/math/exchangeStatus.ts +++ b/sdk/src/math/exchangeStatus.ts @@ -2,7 +2,9 @@ import { ExchangeStatus, isOneOfVariant, PerpMarketAccount, + PerpOperation, SpotMarketAccount, + SpotOperation, StateAccount, } from '../types'; @@ -31,3 +33,10 @@ export function ammPaused( isOneOfVariant(market.status, ['paused', 'ammPaused']) ); } + +export function isOperationPaused( + pausedOperations: number, + operation: PerpOperation | SpotOperation +): boolean { + return (pausedOperations & operation) > 0; +} diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 7a47e8dea..a166d6878 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -237,12 +237,16 @@ export function calculateEntryPrice(userPosition: PerpPosition): BN { * @param userPosition * @returns Precision: PRICE_PRECISION (10^10) */ -export function calculateCostBasis(userPosition: PerpPosition): BN { +export function calculateCostBasis( + userPosition: PerpPosition, + includeSettledPnl = false +): BN { if (userPosition.baseAssetAmount.eq(ZERO)) { return ZERO; } return userPosition.quoteAssetAmount + .add(includeSettledPnl ? userPosition.settledPnl : ZERO) .mul(PRICE_PRECISION) .mul(AMM_TO_QUOTE_PRECISION_RATIO) .div(userPosition.baseAssetAmount) diff --git a/sdk/src/priorityFee/averageOverSlotsStrategy.ts b/sdk/src/priorityFee/averageOverSlotsStrategy.ts index 334100561..c4ba4a135 100644 --- a/sdk/src/priorityFee/averageOverSlotsStrategy.ts +++ b/sdk/src/priorityFee/averageOverSlotsStrategy.ts @@ -1,7 +1,8 @@ +import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; import { PriorityFeeStrategy } from './types'; export class AverageOverSlotsStrategy implements PriorityFeeStrategy { - calculate(samples: { slot: number; prioritizationFee: number }[]): number { + calculate(samples: SolanaPriorityFeeResponse[]): number { if (samples.length === 0) { return 0; } diff --git a/sdk/src/priorityFee/averageStrategy.ts b/sdk/src/priorityFee/averageStrategy.ts index 024d9f315..d5dc99b60 100644 --- a/sdk/src/priorityFee/averageStrategy.ts +++ b/sdk/src/priorityFee/averageStrategy.ts @@ -1,7 +1,8 @@ +import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; import { PriorityFeeStrategy } from './types'; export class AverageStrategy implements PriorityFeeStrategy { - calculate(samples: { slot: number; prioritizationFee: number }[]): number { + calculate(samples: SolanaPriorityFeeResponse[]): number { return ( samples.reduce((a, b) => { return a + b.prioritizationFee; diff --git a/sdk/src/priorityFee/ewmaStrategy.ts b/sdk/src/priorityFee/ewmaStrategy.ts index 7a2296135..d37a0216e 100644 --- a/sdk/src/priorityFee/ewmaStrategy.ts +++ b/sdk/src/priorityFee/ewmaStrategy.ts @@ -1,3 +1,4 @@ +import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; import { PriorityFeeStrategy } from './types'; class EwmaStrategy implements PriorityFeeStrategy { @@ -11,7 +12,7 @@ class EwmaStrategy implements PriorityFeeStrategy { } // samples provided in desc slot order - calculate(samples: { slot: number; prioritizationFee: number }[]): number { + calculate(samples: SolanaPriorityFeeResponse[]): number { if (samples.length === 0) { return 0; } diff --git a/sdk/src/priorityFee/heliusPriorityFeeMethod.ts b/sdk/src/priorityFee/heliusPriorityFeeMethod.ts new file mode 100644 index 000000000..0621a3c92 --- /dev/null +++ b/sdk/src/priorityFee/heliusPriorityFeeMethod.ts @@ -0,0 +1,51 @@ +import fetch from 'node-fetch'; + +export enum HeliusPriorityLevel { + MIN = 'min', // 25th percentile + LOW = 'low', // 25th percentile + MEDIUM = 'medium', // 50th percentile + HIGH = 'high', // 75th percentile + VERY_HIGH = 'veryHigh', // 95th percentile + UNSAFE_MAX = 'unsafeMax', // 100th percentile +} + +export type HeliusPriorityFeeLevels = { + [key in HeliusPriorityLevel]: number; +}; + +export type HeliusPriorityFeeResponse = { + jsonrpc: string; + result: { + priorityFeeEstimate?: number; + priorityFeeLevels?: HeliusPriorityFeeLevels; + }; + id: string; +}; + +/// Fetches the priority fee from the Helius API +/// https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api +export async function fetchHeliusPriorityFee( + heliusRpcUrl: string, + lookbackDistance: number, + addresses: string[] +): Promise { + const response = await fetch(heliusRpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: '1', + method: 'getPriorityFeeEstimate', + params: [ + { + accountKeys: addresses, + options: { + includeAllPriorityFeeLevels: true, + lookbackSlots: lookbackDistance, + }, + }, + ], + }), + }); + return await response.json(); +} diff --git a/sdk/src/priorityFee/index.ts b/sdk/src/priorityFee/index.ts index 4772f9190..55221f834 100644 --- a/sdk/src/priorityFee/index.ts +++ b/sdk/src/priorityFee/index.ts @@ -4,4 +4,6 @@ export * from './ewmaStrategy'; export * from './maxOverSlotsStrategy'; export * from './maxStrategy'; export * from './priorityFeeSubscriber'; +export * from './solanaPriorityFeeMethod'; +export * from './heliusPriorityFeeMethod'; export * from './types'; diff --git a/sdk/src/priorityFee/maxOverSlotsStrategy.ts b/sdk/src/priorityFee/maxOverSlotsStrategy.ts index dfda6a0df..cbaa5f033 100644 --- a/sdk/src/priorityFee/maxOverSlotsStrategy.ts +++ b/sdk/src/priorityFee/maxOverSlotsStrategy.ts @@ -1,7 +1,8 @@ +import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; import { PriorityFeeStrategy } from './types'; export class MaxOverSlotsStrategy implements PriorityFeeStrategy { - calculate(samples: { slot: number; prioritizationFee: number }[]): number { + calculate(samples: SolanaPriorityFeeResponse[]): number { if (samples.length === 0) { return 0; } diff --git a/sdk/src/priorityFee/priorityFeeSubscriber.ts b/sdk/src/priorityFee/priorityFeeSubscriber.ts index faeb9e989..00a4ef838 100644 --- a/sdk/src/priorityFee/priorityFeeSubscriber.ts +++ b/sdk/src/priorityFee/priorityFeeSubscriber.ts @@ -1,16 +1,31 @@ import { Connection, PublicKey } from '@solana/web3.js'; -import { PriorityFeeStrategy } from './types'; +import { + PriorityFeeMethod, + PriorityFeeStrategy, + PriorityFeeSubscriberConfig, +} from './types'; import { AverageOverSlotsStrategy } from './averageOverSlotsStrategy'; import { MaxOverSlotsStrategy } from './maxOverSlotsStrategy'; +import { fetchSolanaPriorityFee } from './solanaPriorityFeeMethod'; +import { + HeliusPriorityFeeLevels, + HeliusPriorityLevel, + fetchHeliusPriorityFee, +} from './heliusPriorityFeeMethod'; export class PriorityFeeSubscriber { connection: Connection; frequencyMs: number; - addresses: PublicKey[]; + addresses: string[]; customStrategy?: PriorityFeeStrategy; averageStrategy = new AverageOverSlotsStrategy(); maxStrategy = new MaxOverSlotsStrategy(); + priorityFeeMethod = PriorityFeeMethod.SOLANA; lookbackDistance: number; + maxFeeMicroLamports?: number; + + heliusRpcUrl?: string; + lastHeliusSample?: HeliusPriorityFeeLevels; intervalId?: ReturnType; @@ -20,32 +35,43 @@ export class PriorityFeeSubscriber { lastMaxStrategyResult = 0; lastSlotSeen = 0; - /** - * @param props - * customStrategy : strategy to return the priority fee to use based on recent samples. defaults to AVERAGE. - */ - public constructor({ - connection, - frequencyMs, - addresses, - customStrategy, - slotsToCheck = 10, - }: { - connection: Connection; - frequencyMs: number; - addresses: PublicKey[]; - customStrategy?: PriorityFeeStrategy; - slotsToCheck?: number; - }) { - this.connection = connection; - this.frequencyMs = frequencyMs; - this.addresses = addresses; - if (!customStrategy) { - this.customStrategy = new AverageOverSlotsStrategy(); + public constructor(config: PriorityFeeSubscriberConfig) { + this.connection = config.connection; + this.frequencyMs = config.frequencyMs; + this.addresses = config.addresses.map((address) => address.toBase58()); + if (config.customStrategy) { + this.customStrategy = config.customStrategy; } else { - this.customStrategy = customStrategy; + this.customStrategy = this.averageStrategy; + } + this.lookbackDistance = config.slotsToCheck ?? 50; + if (config.priorityFeeMethod) { + this.priorityFeeMethod = config.priorityFeeMethod; + + if (this.priorityFeeMethod === PriorityFeeMethod.HELIUS) { + if (config.heliusRpcUrl === undefined) { + if (this.connection.rpcEndpoint.includes('helius')) { + this.heliusRpcUrl = this.connection.rpcEndpoint; + } else { + throw new Error( + 'Connection must be helius, or heliusRpcUrl must be provided to use PriorityFeeMethod.HELIUS' + ); + } + } else { + this.heliusRpcUrl = config.heliusRpcUrl; + } + } + } + + if (this.priorityFeeMethod === PriorityFeeMethod.SOLANA) { + if (this.connection === undefined) { + throw new Error( + 'connection must be provided to use SOLANA priority fee API' + ); + } } - this.lookbackDistance = slotsToCheck; + + this.maxFeeMicroLamports = config.maxFeeMicroLamports; } public async subscribe(): Promise { @@ -53,39 +79,99 @@ export class PriorityFeeSubscriber { return; } + await this.load(); this.intervalId = setInterval(this.load.bind(this), this.frequencyMs); } - public async load(): Promise { - // @ts-ignore - const rpcJSONResponse: any = await this.connection._rpcRequest( - 'getRecentPrioritizationFees', - [this.addresses] + private async loadForSolana(): Promise { + const samples = await fetchSolanaPriorityFee( + this.connection!, + this.lookbackDistance, + this.addresses ); + this.latestPriorityFee = samples[0].prioritizationFee; + this.lastSlotSeen = samples[0].slot; + + this.lastAvgStrategyResult = this.averageStrategy.calculate(samples); + this.lastMaxStrategyResult = this.maxStrategy.calculate(samples); + if (this.customStrategy) { + this.lastCustomStrategyResult = this.customStrategy.calculate(samples); + } + } - const results: { slot: number; prioritizationFee: number }[] = - rpcJSONResponse?.result; + private async loadForHelius(): Promise { + const sample = await fetchHeliusPriorityFee( + this.heliusRpcUrl, + this.lookbackDistance, + this.addresses + ); + this.lastHeliusSample = sample?.result?.priorityFeeLevels ?? undefined; - if (!results.length) return; + if (this.lastHeliusSample) { + this.lastAvgStrategyResult = + this.heliusRpcUrl[HeliusPriorityLevel.MEDIUM]; + this.lastMaxStrategyResult = + this.heliusRpcUrl[HeliusPriorityLevel.UNSAFE_MAX]; + if (this.customStrategy) { + this.lastCustomStrategyResult = this.customStrategy.calculate(sample!); + } + } + } - // # Sort and filter results based on the slot lookback setting - const descResults = results.sort((a, b) => b.slot - a.slot); - const mostRecentResult = descResults[0]; - const cutoffSlot = mostRecentResult.slot - this.lookbackDistance; + public getMaxPriorityFee(): number | undefined { + return this.maxFeeMicroLamports; + } - const resultsToUse = descResults.filter( - (result) => result.slot >= cutoffSlot - ); + public getHeliusPriorityFeeLevel( + level: HeliusPriorityLevel = HeliusPriorityLevel.MEDIUM + ): number { + if (this.lastHeliusSample === undefined) { + return 0; + } + if (this.maxFeeMicroLamports !== undefined) { + return Math.min(this.maxFeeMicroLamports, this.lastHeliusSample[level]); + } + return this.lastHeliusSample[level]; + } - // # Handle results - this.latestPriorityFee = mostRecentResult.prioritizationFee; - this.lastSlotSeen = mostRecentResult.slot; + public getCustomStrategyResult(): number { + if (this.maxFeeMicroLamports !== undefined) { + return Math.min(this.maxFeeMicroLamports, this.lastCustomStrategyResult); + } + return this.lastCustomStrategyResult; + } - this.lastAvgStrategyResult = this.averageStrategy.calculate(resultsToUse); - this.lastMaxStrategyResult = this.maxStrategy.calculate(resultsToUse); - if (this.customStrategy) { - this.lastCustomStrategyResult = - this.customStrategy.calculate(resultsToUse); + public getAvgStrategyResult(): number { + if (this.maxFeeMicroLamports !== undefined) { + return Math.min(this.maxFeeMicroLamports, this.lastAvgStrategyResult); + } + return this.lastAvgStrategyResult; + } + + public getMaxStrategyResult(): number { + if (this.maxFeeMicroLamports !== undefined) { + return Math.min(this.maxFeeMicroLamports, this.lastMaxStrategyResult); + } + return this.lastMaxStrategyResult; + } + + public async load(): Promise { + try { + if (this.priorityFeeMethod === PriorityFeeMethod.SOLANA) { + await this.loadForSolana(); + } else if (this.priorityFeeMethod === PriorityFeeMethod.HELIUS) { + await this.loadForHelius(); + } else { + throw new Error(`${this.priorityFeeMethod} load not implemented`); + } + } catch (err) { + const e = err as Error; + console.error( + `Error loading priority fee ${this.priorityFeeMethod}: ${e.message}\n${ + e.stack ? e.stack : '' + }` + ); + return; } } @@ -95,4 +181,8 @@ export class PriorityFeeSubscriber { this.intervalId = undefined; } } + + public updateAddresses(addresses: PublicKey[]) { + this.addresses = addresses.map((k) => k.toBase58()); + } } diff --git a/sdk/src/priorityFee/solanaPriorityFeeMethod.ts b/sdk/src/priorityFee/solanaPriorityFeeMethod.ts new file mode 100644 index 000000000..9dbed72f1 --- /dev/null +++ b/sdk/src/priorityFee/solanaPriorityFeeMethod.ts @@ -0,0 +1,28 @@ +import { Connection } from '@solana/web3.js'; + +export type SolanaPriorityFeeResponse = { + slot: number; + prioritizationFee: number; +}; + +export async function fetchSolanaPriorityFee( + connection: Connection, + lookbackDistance: number, + addresses: string[] +): Promise { + // @ts-ignore + const rpcJSONResponse: any = await connection._rpcRequest( + 'getRecentPrioritizationFees', + [addresses] + ); + + const results: SolanaPriorityFeeResponse[] = rpcJSONResponse?.result; + + if (!results.length) return; + + // Sort and filter results based on the slot lookback setting + const descResults = results.sort((a, b) => b.slot - a.slot); + const cutoffSlot = descResults[0].slot - lookbackDistance; + + return descResults.filter((result) => result.slot >= cutoffSlot); +} diff --git a/sdk/src/priorityFee/types.ts b/sdk/src/priorityFee/types.ts index 84c4a9c6b..8a04ce962 100644 --- a/sdk/src/priorityFee/types.ts +++ b/sdk/src/priorityFee/types.ts @@ -1,5 +1,35 @@ +import { Connection, PublicKey } from '@solana/web3.js'; +import { SolanaPriorityFeeResponse } from './solanaPriorityFeeMethod'; +import { HeliusPriorityFeeResponse } from './heliusPriorityFeeMethod'; + export interface PriorityFeeStrategy { // calculate the priority fee for a given set of samples. // expect samples to be sorted in descending order (by slot) - calculate(samples: { slot: number; prioritizationFee: number }[]): number; + calculate( + samples: SolanaPriorityFeeResponse[] | HeliusPriorityFeeResponse + ): number; } + +export enum PriorityFeeMethod { + SOLANA = 'solana', + HELIUS = 'helius', +} + +export type PriorityFeeSubscriberConfig = { + /// rpc connection, optional if using priorityFeeMethod.HELIUS + connection?: Connection; + /// frequency to make RPC calls to update priority fee samples, in milliseconds + frequencyMs: number; + /// addresses you plan to write lock, used to determine priority fees + addresses: PublicKey[]; + /// custom strategy to calculate priority fees, defaults to AVERAGE + customStrategy?: PriorityFeeStrategy; + /// method for fetching priority fee samples + priorityFeeMethod?: PriorityFeeMethod; + /// lookback window to determine priority fees, in slots. + slotsToCheck?: number; + /// url for helius rpc, required if using priorityFeeMethod.HELIUS + heliusRpcUrl?: string; + /// clamp any returned priority fee value to this value. + maxFeeMicroLamports?: number; +}; diff --git a/sdk/src/tx/baseTxSender.ts b/sdk/src/tx/baseTxSender.ts index 42d9ad2b9..5780aa1a4 100644 --- a/sdk/src/tx/baseTxSender.ts +++ b/sdk/src/tx/baseTxSender.ts @@ -1,4 +1,9 @@ -import { ConfirmationStrategy, TxSender, TxSigAndSlot } from './types'; +import { + ConfirmationStrategy, + ExtraConfirmationOptions, + TxSender, + TxSigAndSlot, +} from './types'; import { Commitment, ConfirmOptions, @@ -29,6 +34,7 @@ export abstract class BaseTxSender implements TxSender { additionalConnections: Connection[]; timeoutCount = 0; confirmationStrategy: ConfirmationStrategy; + additionalTxSenderCallbacks: ((base58EncodedTx: string) => void)[]; public constructor({ connection, @@ -37,6 +43,7 @@ export abstract class BaseTxSender implements TxSender { timeout = DEFAULT_TIMEOUT, additionalConnections = new Array(), confirmationStrategy = ConfirmationStrategy.Combo, + additionalTxSenderCallbacks, }: { connection: Connection; wallet: IWallet; @@ -44,6 +51,7 @@ export abstract class BaseTxSender implements TxSender { timeout?: number; additionalConnections?; confirmationStrategy?: ConfirmationStrategy; + additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[]; }) { this.connection = connection; this.wallet = wallet; @@ -51,13 +59,15 @@ export abstract class BaseTxSender implements TxSender { this.timeout = timeout; this.additionalConnections = additionalConnections; this.confirmationStrategy = confirmationStrategy; + this.additionalTxSenderCallbacks = additionalTxSenderCallbacks; } async send( tx: Transaction, additionalSigners?: Array, opts?: ConfirmOptions, - preSigned?: boolean + preSigned?: boolean, + extraConfirmationOptions?: ExtraConfirmationOptions ): Promise { if (additionalSigners === undefined) { additionalSigners = []; @@ -70,6 +80,10 @@ export abstract class BaseTxSender implements TxSender { ? tx : await this.prepareTx(tx, additionalSigners, opts); + if (extraConfirmationOptions?.onSignedCb) { + extraConfirmationOptions.onSignedCb(); + } + return this.sendRawTransaction(signedTx.serialize(), opts); } @@ -124,7 +138,8 @@ export abstract class BaseTxSender implements TxSender { tx: VersionedTransaction, additionalSigners?: Array, opts?: ConfirmOptions, - preSigned?: boolean + preSigned?: boolean, + extraConfirmationOptions?: ExtraConfirmationOptions ): Promise { let signedTx; if (preSigned) { @@ -144,6 +159,10 @@ export abstract class BaseTxSender implements TxSender { signedTx = await this.wallet.signTransaction(tx); } + if (extraConfirmationOptions?.onSignedCb) { + extraConfirmationOptions.onSignedCb(); + } + if (opts === undefined) { opts = this.opts; } @@ -247,9 +266,11 @@ export abstract class BaseTxSender implements TxSender { commitment: Commitment = 'finalized' ): Promise | undefined> { let totalTime = 0; - let backoffTime = 250; + let backoffTime = 400; // approx block time while (totalTime < this.timeout) { + await new Promise((resolve) => setTimeout(resolve, backoffTime)); + const response = await this.connection.getSignatureStatus(signature); const result = response && response.value?.[0]; @@ -257,7 +278,6 @@ export abstract class BaseTxSender implements TxSender { return { context: result.context, value: { err: null } }; } - await new Promise((resolve) => setTimeout(resolve, backoffTime)); totalTime += backoffTime; backoffTime = Math.min(backoffTime * 2, 5000); } @@ -317,6 +337,9 @@ export abstract class BaseTxSender implements TxSender { console.error(e); }); }); + this.additionalTxSenderCallbacks?.map((callback) => { + callback(bs58.encode(rawTx)); + }); } public addAdditionalConnection(newConnection: Connection): void { diff --git a/sdk/src/tx/retryTxSender.ts b/sdk/src/tx/retryTxSender.ts index 5ac8d23f1..d9b77f836 100644 --- a/sdk/src/tx/retryTxSender.ts +++ b/sdk/src/tx/retryTxSender.ts @@ -1,9 +1,5 @@ import { ConfirmationStrategy, TxSigAndSlot } from './types'; -import { - ConfirmOptions, - TransactionSignature, - Connection, -} from '@solana/web3.js'; +import { ConfirmOptions, Connection } from '@solana/web3.js'; import { AnchorProvider } from '@coral-xyz/anchor'; import { IWallet } from '../types'; import { BaseTxSender } from './baseTxSender'; @@ -32,6 +28,7 @@ export class RetryTxSender extends BaseTxSender { retrySleep = DEFAULT_RETRY, additionalConnections = new Array(), confirmationStrategy = ConfirmationStrategy.Combo, + additionalTxSenderCallbacks = [], }: { connection: Connection; wallet: IWallet; @@ -40,6 +37,7 @@ export class RetryTxSender extends BaseTxSender { retrySleep?: number; additionalConnections?; confirmationStrategy?: ConfirmationStrategy; + additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[]; }) { super({ connection, @@ -48,6 +46,7 @@ export class RetryTxSender extends BaseTxSender { timeout, additionalConnections, confirmationStrategy, + additionalTxSenderCallbacks, }); this.connection = connection; this.wallet = wallet; @@ -70,14 +69,8 @@ export class RetryTxSender extends BaseTxSender { ): Promise { const startTime = this.getTimestamp(); - let txid: TransactionSignature; - try { - txid = await this.connection.sendRawTransaction(rawTransaction, opts); - this.sendToAdditionalConnections(rawTransaction, opts); - } catch (e) { - console.error(e); - throw e; - } + const txid = await this.connection.sendRawTransaction(rawTransaction, opts); + this.sendToAdditionalConnections(rawTransaction, opts); let done = false; const resolveReference: ResolveReference = { @@ -109,8 +102,8 @@ export class RetryTxSender extends BaseTxSender { try { const result = await this.confirmTransaction(txid, opts.commitment); slot = result.context.slot; + // eslint-disable-next-line no-useless-catch } catch (e) { - console.error(e); throw e; } finally { stopWaiting(); diff --git a/sdk/src/tx/types.ts b/sdk/src/tx/types.ts index a76c04fe6..360699685 100644 --- a/sdk/src/tx/types.ts +++ b/sdk/src/tx/types.ts @@ -20,6 +20,10 @@ export type TxSigAndSlot = { slot: number; }; +export type ExtraConfirmationOptions = { + onSignedCb: () => void; +}; + export interface TxSender { wallet: IWallet; @@ -27,14 +31,16 @@ export interface TxSender { tx: Transaction, additionalSigners?: Array, opts?: ConfirmOptions, - preSigned?: boolean + preSigned?: boolean, + extraConfirmationOptions?: ExtraConfirmationOptions ): Promise; sendVersionedTransaction( tx: VersionedTransaction, additionalSigners?: Array, opts?: ConfirmOptions, - preSigned?: boolean + preSigned?: boolean, + extraConfirmationOptions?: ExtraConfirmationOptions ): Promise; getVersionedTransaction( diff --git a/sdk/src/tx/utils.ts b/sdk/src/tx/utils.ts index ae3437f69..cae58e362 100644 --- a/sdk/src/tx/utils.ts +++ b/sdk/src/tx/utils.ts @@ -1,7 +1,9 @@ +import { Wallet } from '@coral-xyz/anchor'; import { Transaction, TransactionInstruction, ComputeBudgetProgram, + VersionedTransaction, } from '@solana/web3.js'; const COMPUTE_UNITS_DEFAULT = 200_000; @@ -30,3 +32,33 @@ export function wrapInTx( return tx.add(instruction); } + +/* Helper function for signing multiple transactions where some may be undefined and mapping the output */ +export async function getSignedTransactionMap( + wallet: Wallet, + txsToSign: (Transaction | VersionedTransaction | undefined)[], + keys: string[] +): Promise<{ [key: string]: Transaction | VersionedTransaction | undefined }> { + const signedTxMap: { + [key: string]: Transaction | VersionedTransaction | undefined; + } = {}; + + const keysWithTx = []; + txsToSign.forEach((tx, index) => { + if (tx == undefined) { + signedTxMap[keys[index]] = undefined; + } else { + keysWithTx.push(keys[index]); + } + }); + + const signedTxs = await wallet.signAllTransactions( + txsToSign.filter((tx) => tx !== undefined) + ); + + signedTxs.forEach((signedTx, index) => { + signedTxMap[keysWithTx[index]] = signedTx; + }); + + return signedTxMap; +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 415d45764..515ad92f9 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -27,6 +27,20 @@ export class MarketStatus { static readonly DELISTED = { delisted: {} }; } +export enum PerpOperation { + UPDATE_FUNDING = 1, + AMM_FILL = 2, + FILL = 4, + SETTLE_PNL = 8, + SETTLE_PNL_WITH_POSITION = 16, +} + +export enum SpotOperation { + UPDATE_CUMULATIVE_INTEREST = 1, + FILL = 2, + WITHDRAW = 4, +} + export enum UserStatus { BEING_LIQUIDATED = 1, BANKRUPT = 2, diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 2a994e485..576cd17cb 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -388,12 +388,16 @@ export class User { /** * 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): [BN, BN] { + public getLPBidAsks(marketIndex: number, lpShares?: BN): [BN, BN] { const position = this.getPerpPosition(marketIndex); - if (position === undefined || position.lpShares.eq(ZERO)) { + + const lpSharesToCalc = lpShares ?? position?.lpShares; + + if (!lpSharesToCalc || lpSharesToCalc.eq(ZERO)) { return [ZERO, ZERO]; } @@ -405,12 +409,8 @@ export class User { market.amm.orderStepSize ); - const lpOpenBids = marketOpenBids - .mul(position.lpShares) - .div(market.amm.sqrtK); - const lpOpenAsks = marketOpenAsks - .mul(position.lpShares) - .div(market.amm.sqrtK); + const lpOpenBids = marketOpenBids.mul(lpSharesToCalc).div(market.amm.sqrtK); + const lpOpenAsks = marketOpenAsks.mul(lpSharesToCalc).div(market.amm.sqrtK); return [lpOpenBids, lpOpenAsks]; } @@ -766,7 +766,9 @@ export class User { strict = false ): BN { return this.getActivePerpPositions() - .filter((pos) => (marketIndex ? pos.marketIndex === marketIndex : true)) + .filter((pos) => + marketIndex !== undefined ? pos.marketIndex === marketIndex : true + ) .reduce((unrealizedPnl, perpPosition) => { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex @@ -3291,6 +3293,10 @@ export class User { }; for (const perpPosition of this.getActivePerpPositions()) { + const settledLpPosition = this.getPerpPositionWithLPSettle( + perpPosition.marketIndex, + perpPosition + )[0]; const perpMarket = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); @@ -3299,7 +3305,7 @@ export class User { ).data; const oraclePrice = oraclePriceData.price; const worstCaseBaseAmount = - calculateWorstCaseBaseAssetAmount(perpPosition); + calculateWorstCaseBaseAssetAmount(settledLpPosition); const marginRatio = new BN( calculateMarketMarginRatio( diff --git a/sdk/src/userMap/userMap.ts b/sdk/src/userMap/userMap.ts index b7ade3043..697190e2c 100644 --- a/sdk/src/userMap/userMap.ts +++ b/sdk/src/userMap/userMap.ts @@ -303,65 +303,59 @@ export class UserMap implements UserMapInterface { }, ]; - const rpcJSONResponse: any = - // @ts-ignore - await this.connection._rpcRequest('getProgramAccounts', rpcRequestArgs); - - const rpcResponseAndContext: RpcResponseAndContext< - Array<{ - pubkey: PublicKey; - account: { - data: [string, string]; - }; - }> - > = rpcJSONResponse.result; - + // @ts-ignore + const rpcJSONResponse: any = await this.connection._rpcRequest('getProgramAccounts', rpcRequestArgs); + const rpcResponseAndContext: RpcResponseAndContext> = rpcJSONResponse.result; const slot = rpcResponseAndContext.context.slot; - this.updateLatestSlot(slot); const programAccountBufferMap = new Map(); - for (const programAccount of rpcResponseAndContext.value) { + rpcResponseAndContext.value.forEach(programAccount => { programAccountBufferMap.set( programAccount.pubkey.toString(), // @ts-ignore - Buffer.from( - programAccount.account.data[0], - programAccount.account.data[1] - ) + Buffer.from(programAccount.account.data[0], programAccount.account.data[1]) ); - } + }); + + const concurrencyLimit = 20; + const semaphore = new Array(concurrencyLimit).fill(Promise.resolve()); - for (const [key, buffer] of programAccountBufferMap.entries()) { + const processAccount = async (key: string, buffer: Buffer) => { if (!this.has(key)) { const userAccount = this.decode('User', buffer); await this.addPubkey(new PublicKey(key), userAccount); - this.userMap.get(key).accountSubscriber.updateData(userAccount, slot); + this.userMap.get(key)?.accountSubscriber.updateData(userAccount, slot); } else { const userAccount = this.decode('User', buffer); - this.userMap.get(key).accountSubscriber.updateData(userAccount, slot); + this.userMap.get(key)?.accountSubscriber.updateData(userAccount, slot); } - // give event loop a chance to breathe - await new Promise((resolve) => setTimeout(resolve, 0)); - } + }; + + const promises = Array.from(programAccountBufferMap.entries()).map(async ([key, buffer]) => { + const index = await Promise.race(semaphore.map((p, index) => p.then(() => index))); + semaphore[index] = processAccount(key, buffer).then(() => { + return; + }); + }); + + await Promise.all(promises.concat(semaphore)); for (const [key, user] of this.userMap.entries()) { if (!programAccountBufferMap.has(key)) { await user.unsubscribe(); this.userMap.delete(key); } - // give event loop a chance to breathe - await new Promise((resolve) => setTimeout(resolve, 0)); } } catch (e) { - console.error(`Error in UserMap.sync():`); - console.error(e); + console.error(`Error in UserMap.sync():`, e); } finally { this.syncPromiseResolver(); this.syncPromise = undefined; } } + public async unsubscribe() { await this.subscription.unsubscribe(); @@ -404,3 +398,4 @@ export class UserMap implements UserMapInterface { return this.mostRecentSlot; } } + diff --git a/sdk/tests/amm/test.ts b/sdk/tests/amm/test.ts index abc75015f..d83d38401 100644 --- a/sdk/tests/amm/test.ts +++ b/sdk/tests/amm/test.ts @@ -31,6 +31,7 @@ import { ContractTier, isOracleValid, OracleGuardRails, + // calculateReservePrice, } from '../../src'; import { mockPerpMarkets } from '../dlob/helpers'; @@ -437,11 +438,242 @@ describe('AMM Tests', () => { true ); - console.log(terms2); + // console.log(terms2); assert(terms2.effectiveLeverageCapped <= 1.000001); assert(terms2.inventorySpreadScale == 1.0306); assert(terms2.longSpread == 515); assert(terms2.shortSpread == 5668); + + const suiExample = { + status: 'active', + contractType: 'perpetual', + contractTier: 'c', + expiryTs: '0', + expiryPrice: '0', + marketIndex: 9, + pubkey: '91NsaUmTNNdLGbYtwmoiYSn9SgWHCsZiChfMYMYZ2nQx', + name: 'SUI-PERP', + amm: { + baseAssetReserve: '234381482764434', + sqrtK: '109260723000000001', + lastFundingRate: '-16416', + lastFundingRateTs: '1705845755', + lastMarkPriceTwap: '1105972', + lastMarkPriceTwap5Min: '1101202', + lastMarkPriceTwapTs: '1705846920', + lastTradeTs: '1705846920', + oracle: '3Qub3HaAJaa2xNY7SUqPKd3vVwTqDfDDkEUMPjXD2c1q', + oracleSource: 'pyth', + historicalOracleData: { + lastOraclePrice: '1099778', + lastOracleDelay: '2', + lastOracleConf: '0', + lastOraclePriceTwap: '1106680', + lastOraclePriceTwap5Min: '1102634', + lastOraclePriceTwapTs: '1705846920', + }, + lastOracleReservePriceSpreadPct: '-262785', + lastOracleConfPct: '1359', + fundingPeriod: '3600', + quoteAssetReserve: '50933655038273508156', + pegMultiplier: '4', + cumulativeFundingRateLong: '186069301', + cumulativeFundingRateShort: '186007157', + last24HAvgFundingRate: '35147', + lastFundingRateShort: '-16416', + lastFundingRateLong: '-16416', + totalLiquidationFee: '4889264000', + totalFeeMinusDistributions: '-29523583393', + totalFeeWithdrawn: '5251194706', + totalFee: '7896066035', + totalFeeEarnedPerLp: '77063238', + userLpShares: '109260723000000000', + baseAssetAmountWithUnsettledLp: '-762306519581', + orderStepSize: '1000000000', + orderTickSize: '100', + maxFillReserveFraction: '100', + maxSlippageRatio: '50', + baseSpread: '5000', + curveUpdateIntensity: '100', + baseAssetAmountWithAmm: '306519581', + baseAssetAmountLong: '223405000000000', + baseAssetAmountShort: '-224167000000000', + quoteAssetAmount: '57945607973', + terminalQuoteAssetReserve: '50933588428309274920', + concentrationCoef: '1207100', + feePool: '[object Object]', + totalExchangeFee: '10110336057', + totalMmFee: '-1870961568', + netRevenueSinceLastFunding: '-141830281', + lastUpdateSlot: '243204071', + lastOracleNormalisedPrice: '1098594', + lastOracleValid: 'true', + lastBidPriceTwap: '1105864', + lastAskPriceTwap: '1106081', + longSpread: '259471', + shortSpread: '3314', + maxSpread: '29500', + baseAssetAmountPerLp: '-11388426214145', + quoteAssetAmountPerLp: '13038990874', + targetBaseAssetAmountPerLp: '0', + ammJitIntensity: '200', + maxOpenInterest: '2000000000000000', + maxBaseAssetReserve: '282922257844734', + minBaseAssetReserve: '194169322578092', + totalSocialLoss: '0', + quoteBreakEvenAmountLong: '-237442196125', + quoteBreakEvenAmountShort: '243508341566', + quoteEntryAmountLong: '-234074123777', + quoteEntryAmountShort: '240215285058', + markStd: '237945', + oracleStd: '8086', + longIntensityCount: '0', + longIntensityVolume: '162204', + shortIntensityCount: '995', + shortIntensityVolume: '2797331131', + volume24H: '91370028405', + minOrderSize: '1000000000', + maxPositionSize: '0', + bidBaseAssetReserve: '234770820775670', + bidQuoteAssetReserve: '50849187948657797529', + askBaseAssetReserve: '205083797418879', + askQuoteAssetReserve: '58209891472312580749', + perLpBase: '4', + }, + numberOfUsersWithBase: '279', + numberOfUsers: '436', + marginRatioInitial: '1000', + marginRatioMaintenance: '500', + nextFillRecordId: '69433', + nextFundingRateRecordId: '6221', + nextCurveRecordId: '1731', + pnlPool: { + scaledBalance: '61514197782399', + marketIndex: '0', + }, + liquidatorFee: '10000', + ifLiquidationFee: '20000', + imfFactor: '450', + unrealizedPnlImfFactor: '450', + unrealizedPnlMaxImbalance: '200000000', + unrealizedPnlInitialAssetWeight: '0', + unrealizedPnlMaintenanceAssetWeight: '10000', + insuranceClaim: { + revenueWithdrawSinceLastSettle: '100000000', + maxRevenueWithdrawPerPeriod: '100000000', + lastRevenueWithdrawTs: '1705846454', + quoteSettledInsurance: '164388488', + quoteMaxInsurance: '1000000000', + }, + quoteSpotMarketIndex: '0', + feeAdjustment: '0', + }; + + const reservePrice = calculatePrice( + new BN(suiExample.amm.baseAssetReserve), + new BN(suiExample.amm.quoteAssetReserve), + new BN(suiExample.amm.pegMultiplier) + ); + console.log('reservePrice', reservePrice.toString()); + assert(reservePrice.eq(new BN('869243'))); + + const reservePriceMod = calculatePrice( + new BN(suiExample.amm.baseAssetReserve), + new BN(suiExample.amm.quoteAssetReserve), + new BN(suiExample.amm.pegMultiplier).add(ONE) + ); + console.log('reservePriceMod', reservePriceMod.toString()); + assert(reservePriceMod.eq(new BN('1086554'))); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const termsSuiExample: AMMSpreadTerms = calculateSpreadBN( + Number(suiExample.amm.baseSpread.toString()), + new BN(suiExample.amm.lastOracleReservePriceSpreadPct), + new BN(suiExample.amm.lastOracleConfPct), + Number(suiExample.amm.maxSpread.toString()), + new BN(suiExample.amm.quoteAssetReserve), + new BN(suiExample.amm.terminalQuoteAssetReserve), + new BN(suiExample.amm.pegMultiplier), + new BN(suiExample.amm.baseAssetAmountWithAmm), + reservePrice, // reserve price + new BN(suiExample.amm.totalFeeMinusDistributions), + new BN(suiExample.amm.netRevenueSinceLastFunding), + new BN(suiExample.amm.baseAssetReserve), + new BN(suiExample.amm.minBaseAssetReserve), + new BN(suiExample.amm.maxBaseAssetReserve), + new BN(suiExample.amm.markStd), + new BN(suiExample.amm.oracleStd), + new BN(suiExample.amm.longIntensityVolume), + new BN(suiExample.amm.shortIntensityVolume), + new BN(suiExample.amm.volume24H), + true + ); + + // console.log(termsSuiExample); + assert(termsSuiExample.effectiveLeverageCapped <= 1.000001); + assert(termsSuiExample.inventorySpreadScale == 1.00007); + assert(termsSuiExample.longSpread == 259073); + assert(termsSuiExample.shortSpread == 3712); + + // reset amm reserves/peg to balanced values s.t. liquidity/price is the same + // to avoid error prone int math + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const termsSuiExampleMod1: AMMSpreadTerms = calculateSpreadBN( + Number(suiExample.amm.baseSpread.toString()), + ZERO, + new BN(suiExample.amm.lastOracleConfPct), + Number(suiExample.amm.maxSpread.toString()), + new BN(suiExample.amm.quoteAssetReserve), + new BN(suiExample.amm.terminalQuoteAssetReserve), + new BN(suiExample.amm.pegMultiplier), + new BN(suiExample.amm.baseAssetAmountWithAmm), + reservePriceMod, // reserve price + new BN(suiExample.amm.totalFeeMinusDistributions), + new BN(suiExample.amm.netRevenueSinceLastFunding), + new BN(suiExample.amm.baseAssetReserve), + new BN(suiExample.amm.minBaseAssetReserve), + new BN(suiExample.amm.maxBaseAssetReserve), + new BN(suiExample.amm.markStd), + new BN(suiExample.amm.oracleStd), + new BN(suiExample.amm.longIntensityVolume), + new BN(suiExample.amm.shortIntensityVolume), + new BN(suiExample.amm.volume24H), + true + ); + console.log(termsSuiExampleMod1); + + // todo: add sdk recenter function? + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const termsSuiExampleMod2: AMMSpreadTerms = calculateSpreadBN( + Number(suiExample.amm.baseSpread.toString()), + ZERO, + new BN(suiExample.amm.lastOracleConfPct), + Number(suiExample.amm.maxSpread.toString()), + new BN(suiExample.amm.sqrtK), + new BN(suiExample.amm.terminalQuoteAssetReserve), + reservePriceMod, // peg + new BN(suiExample.amm.baseAssetAmountWithAmm), + reservePriceMod, // reserve price + new BN(suiExample.amm.totalFeeMinusDistributions), + new BN(suiExample.amm.netRevenueSinceLastFunding), + new BN(suiExample.amm.sqrtK), + new BN(suiExample.amm.sqrtK.sub()), + new BN(suiExample.amm.maxBaseAssetReserve), + new BN(suiExample.amm.markStd), + new BN(suiExample.amm.oracleStd), + new BN(suiExample.amm.longIntensityVolume), + new BN(suiExample.amm.shortIntensityVolume), + new BN(suiExample.amm.volume24H), + true + ); + + console.log(termsSuiExampleMod2); + // assert(_.isEqual(termsSuiExampleMod2, termsSuiExampleMod1)); }); it('Spread Reserves (with offset)', () => { diff --git a/sdk/tests/tx/priorityFeeStrategy.ts b/sdk/tests/tx/priorityFeeStrategy.ts index 1296e0a89..4e09d08a5 100644 --- a/sdk/tests/tx/priorityFeeStrategy.ts +++ b/sdk/tests/tx/priorityFeeStrategy.ts @@ -76,7 +76,7 @@ describe('PriorityFeeStrategy', () => { { slot: 1, prioritizationFee: 1000 }, ]; const maxOverSlots = maxOverSlotsStrategy.calculate(samples); - expect(maxOverSlots).to.equal(832); + expect(maxOverSlots).to.equal(1000); }); it('AverageOverSlotsStrategy should calculate the average prioritization fee over slots', () => { @@ -90,6 +90,6 @@ describe('PriorityFeeStrategy', () => { { slot: 1, prioritizationFee: 1000 }, ]; const averageOverSlots = averageOverSlotsStrategy.calculate(samples); - expect(averageOverSlots).to.equal(454.4); + expect(averageOverSlots).to.approximately(545.33333, 0.00001); }); }); diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 870c61e11..5f3e6c2ef 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -9,6 +9,7 @@ test_files=( stopLimits.ts oracleFillPriceGuardrails.ts perpLpJit.ts + perpLpRiskMitigation.ts spotSwap.ts maxLeverageOrderParams.ts multipleMakerOrders.ts diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 9a9bcef68..661c0378c 100644 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -4,7 +4,7 @@ if [ "$1" != "--skip-build" ] cp target/idl/drift.json sdk/src/idl/ fi -test_files=(perpLpJit.ts) +test_files=(perpLpRiskMitigation.ts) for test_file in ${test_files[@]}; do ANCHOR_TEST_FILE=${test_file} anchor test --skip-build || exit 1; diff --git a/tests/perpLpRiskMitigation.ts b/tests/perpLpRiskMitigation.ts new file mode 100644 index 000000000..db8365c9f --- /dev/null +++ b/tests/perpLpRiskMitigation.ts @@ -0,0 +1,492 @@ +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, + BulkAccountLoader, + isVariant, + MARGIN_PRECISION, +} from '../sdk/src'; + +import { + initializeQuoteSpotMarket, + mockOracle, + mockUSDCMint, + mockUserUSDCAccount, + setFeedPrice, + sleep, + // sleep, +} from './testHelpers'; + +async function adjustOraclePostSwap(baa, swapDirection, market) { + 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 setFeedPrice(anchor.workspace.Pyth, _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 +) { + let walletFlag = true; + if (wallet == undefined) { + const kp = new web3.Keypair(); + const sig = await provider.connection.requestAirdrop(kp.publicKey, 10 ** 9); + await provider.connection.confirmTransaction(sig); + 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, 2, 3], + spotMarketIndexes: [0], + 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(), + }); + driftClientUser.subscribe(); + + return [driftClient, driftClientUser]; +} + +describe('lp risk mitigation', () => { + const provider = anchor.AnchorProvider.local(undefined, { + preflightCommitment: 'confirmed', + commitment: 'confirmed', + }); + const connection = provider.connection; + anchor.setProvider(provider); + const chProgram = anchor.workspace.Drift as Program; + + async function _viewLogs(txsig) { + const tx = await 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(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; + const eventSubscriber = new EventSubscriber(connection, chProgram, { + commitment: 'recent', + }); + eventSubscriber.subscribe(); + + const bulkAccountLoader = new BulkAccountLoader(connection, 'confirmed', 1); + + 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 () => { + usdcMint = await mockUSDCMint(provider); + + solusdc3 = await mockOracle(1, -7); // make invalid + solusdc2 = await mockOracle(1, -7); // make invalid + solusdc = await mockOracle(1, -7); // make invalid + btcusdc = await mockOracle(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, + provider, + usdcMint, + usdcAmount, + oracleInfos, + 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, + provider, + usdcMint, + usdcAmount, + oracleInfos, + undefined, + bulkAccountLoader + ); + [poorDriftClient, poorDriftClientUser] = await createNewUser( + chProgram, + provider, + 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 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(3000 * 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('13000000000000'))); + assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); + assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); + + let user = await driftClientUser.getUserAccount(); + assert(user.perpPositions[0].lpShares.toString() == '3000000000000'); // 3000 * 1e9 + + // lp goes short + const tradeSize = new BN(500 * BASE_PRECISION.toNumber()); + try { + await adjustOraclePostSwap(tradeSize, SwapDirection.REMOVE, market); + const _txsig = await driftClient.openPosition( + PositionDirection.SHORT, + 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('38461538'))); + + // some user goes long (lp should get more short) + try { + await adjustOraclePostSwap(tradeSize, SwapDirection.REMOVE, market); + 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 = 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)); + + const perpLiqPrice = driftClientUser.liquidationPrice(0); + console.log('perpLiqPrice:', perpLiqPrice.toString()); + + await setFeedPrice(anchor.workspace.Pyth, 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(); + assert(driftClientUser.getUserAccount().openOrders == 0); + + console.log('settling after margin ratio update...'); + try { + const _txsigg = await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + await driftClient.getUserAccount(), + 0 + ); + 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(ZERO)); + 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')); + + assert( + afterReduceOrdersAccount.perpPositions[0].lpShares.eq( + new BN(2000 * BASE_PRECISION.toNumber()) + ) + ); + }); +});