From 66a559acb218e420d3feab11debdf751df2f3dbd Mon Sep 17 00:00:00 2001 From: 0xbigz <83473873+0xbigz@users.noreply.github.com> Date: Wed, 27 Dec 2023 17:19:28 -0500 Subject: [PATCH 1/3] bigz/improve-perp-match-get_fallback_price --- programs/drift/src/controller/orders.rs | 13 ++++----- programs/drift/src/math/orders.rs | 20 ------------- programs/drift/src/state/perp_market.rs | 39 +++++++++++++++++++++++++ programs/drift/src/state/user.rs | 4 +++ 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 85d2f0595..6efa9f001 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -2089,25 +2089,22 @@ pub fn fulfill_perp_order_with_match( return Ok((0_u64, 0_u64, 0_u64)); } - let (bid_price, ask_price) = market.amm.bid_ask_price(market.amm.reserve_price()?)?; - let oracle_price = oracle_map.get_price_data(&market.amm.oracle)?.price; - let taker_direction = taker.orders[taker_order_index].direction; + let taker_direction: PositionDirection = taker.orders[taker_order_index].direction; let taker_price = if let Some(taker_limit_price) = taker_limit_price { taker_limit_price } else { let amm_available_liquidity = calculate_amm_available_liquidity(&market.amm, &taker_direction)?; - get_fallback_price( + market.amm.get_fallback_price( &taker_direction, - bid_price, - ask_price, amm_available_liquidity, oracle_price, - )? + taker.orders[taker_order_index].seconds_til_expiry(now) + )?.cast()? }; - + let taker_existing_position = taker .get_perp_position(market.market_index)? .base_asset_amount; diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 8f92a0ee2..944512620 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -693,26 +693,6 @@ pub fn calculate_fill_price( .cast::() } -pub fn get_fallback_price( - direction: &PositionDirection, - bid_price: u64, - ask_price: u64, - amm_available_liquidity: u64, - oracle_price: i64, -) -> DriftResult { - let oracle_price = oracle_price.unsigned_abs(); - match direction { - PositionDirection::Long if amm_available_liquidity > 0 => { - ask_price.safe_add(ask_price / 200) - } - PositionDirection::Long => oracle_price.safe_add(oracle_price / 20), - PositionDirection::Short if amm_available_liquidity > 0 => { - bid_price.safe_sub(bid_price / 200) - } - PositionDirection::Short => oracle_price.safe_sub(oracle_price / 20), - } -} - pub fn get_max_fill_amounts( user: &User, user_order_index: usize, diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index eef2b9a02..148246ac8 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -771,6 +771,45 @@ impl Default for AMM { } impl AMM { + pub fn get_fallback_price(self, + direction: &PositionDirection, + amm_available_liquidity: u64, + oracle_price: i64, + seconds_til_order_expiry: i64, + ) -> DriftResult { + // PRICE_PRECISION + if direction.eq(&PositionDirection::Long) { + // pick amm ask + buffer if theres liquidity + // otherwise be aggressive vs oracle + 1hr premium + if amm_available_liquidity >= self.min_order_size { + let reserve_price = self.reserve_price()?; + let amm_ask_price: i64 = self.ask_price(reserve_price)?.cast()?; + amm_ask_price + .safe_add(amm_ask_price / (seconds_til_order_expiry * 20).clamp(100, 200)) + } else { + oracle_price.safe_add(self.last_ask_price_twap.cast::()? + .safe_sub(self.historical_oracle_data.last_oracle_price_twap)? + .max(0) + )?.safe_add(oracle_price / (seconds_til_order_expiry * 2).clamp(10, 50)) + } + } else { + // pick amm bid - buffer if theres liquidity + // otherwise be aggressive vs oracle + 1hr bid premium + if amm_available_liquidity >= self.min_order_size { + let reserve_price = self.reserve_price()?; + let amm_bid_price: i64 = self.bid_price(reserve_price)?.cast()?; + amm_bid_price + .safe_sub(amm_bid_price / (seconds_til_order_expiry * 20).clamp(100, 200)) + } else { + oracle_price.safe_add(self.last_bid_price_twap.cast::()? + .safe_sub(self.historical_oracle_data.last_oracle_price_twap)? + .min(0) + )?.safe_add(oracle_price / (seconds_til_order_expiry * 2).clamp(10, 50)) + } + } + + } + pub fn get_protocol_owned_position(self) -> DriftResult { self.base_asset_amount_with_amm .safe_add(self.base_asset_amount_with_unsettled_lp)? diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 21ec1ba33..c5135bbd1 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -999,6 +999,10 @@ pub enum AssetType { } impl Order { + pub fn seconds_til_expiry(self, now: i64) -> i64 { + self.max_ts - now + } + pub fn has_oracle_price_offset(self) -> bool { self.oracle_price_offset != 0 } From 32164be0be9a0fbbc0fb4a5b50b1314e4d32d667 Mon Sep 17 00:00:00 2001 From: 0xbigz <83473873+0xbigz@users.noreply.github.com> Date: Fri, 29 Dec 2023 15:53:50 -0500 Subject: [PATCH 2/3] add tests --- programs/drift/src/controller/orders.rs | 6 +- programs/drift/src/instructions/spot.rs | 100 +++++++++++++++++ programs/drift/src/math/orders/tests.rs | 143 ++++++++++++++++++++++++ programs/drift/src/state/perp_market.rs | 47 +++++--- programs/drift/src/state/user.rs | 2 +- sdk/src/driftClient.ts | 10 +- 6 files changed, 283 insertions(+), 25 deletions(-) create mode 100644 programs/drift/src/instructions/spot.rs diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6efa9f001..2c5ceaa22 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -2101,10 +2101,10 @@ pub fn fulfill_perp_order_with_match( &taker_direction, amm_available_liquidity, oracle_price, - taker.orders[taker_order_index].seconds_til_expiry(now) - )?.cast()? + taker.orders[taker_order_index].seconds_til_expiry(now), + )? }; - + let taker_existing_position = taker .get_perp_position(market.market_index)? .base_asset_amount; diff --git a/programs/drift/src/instructions/spot.rs b/programs/drift/src/instructions/spot.rs new file mode 100644 index 000000000..0380ceb29 --- /dev/null +++ b/programs/drift/src/instructions/spot.rs @@ -0,0 +1,100 @@ + +use anchor_lang::prelude::*; +use anchor_lang::Discriminator; +use anchor_spl::token::{Token, TokenAccount}; +use solana_program::program::invoke; +use solana_program::system_instruction::transfer; + +use crate::controller::orders::{cancel_orders, ModifyOrderId}; +use crate::controller::position::PositionDirection; +use crate::controller::spot_balance::update_revenue_pool_balances; +use crate::controller::spot_position::{ + charge_withdraw_fee, update_spot_balances_and_cumulative_deposits, + update_spot_balances_and_cumulative_deposits_with_limits, +}; +use crate::error::ErrorCode; +use crate::ids::{ + jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, marinade_mainnet, serum_program, +}; +use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::{ + get_maker_and_maker_stats, get_referrer_and_referrer_stats, get_whitelist_token, load_maps, + AccountMaps, +}; +use crate::instructions::SpotFulfillmentType; +use crate::load_mut; +use crate::math::casting::Cast; +use crate::math::liquidation::is_user_being_liquidated; +use crate::math::margin::{ + calculate_max_withdrawable_amount, meets_initial_margin_requirement, + meets_withdraw_margin_requirement, validate_spot_margin_trading, MarginRequirementType, +}; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_value; +use crate::math::spot_swap; +use crate::math::spot_swap::{calculate_swap_price, validate_price_bands_for_swap}; +use crate::math_error; +use crate::print_error; +use crate::safe_decrement; +use crate::safe_increment; +use crate::state::events::{ + SwapSpotPoolRecord, +}; +use crate::state::fill_mode::FillMode; +use crate::state::fulfillment_params::drift::MatchFulfillmentParams; +use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; +use crate::state::fulfillment_params::serum::SerumFulfillmentParams; +use crate::state::oracle::StrictOraclePrice; +use crate::state::order_params::{ + ModifyOrderParams, OrderParams, PlaceOrderOptions, PostOnlyParam, +}; +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; +use crate::state::spot_market::SpotBalanceType; +use crate::state::spot_market::SpotMarket; +use crate::state::spot_market_map::{ + get_writable_spot_market_set, get_writable_spot_market_set_from_many, +}; +use crate::state::state::State; +use crate::state::traits::Size; +use crate::state::user::{MarketType, OrderType, ReferrerName, User, UserStats}; +use crate::state::user_map::load_user_maps; +use crate::validate; +use crate::validation::user::validate_user_deletion; +use crate::validation::whitelist::validate_whitelist_token; +use crate::{controller, math}; +use crate::{get_then_update_id, QUOTE_SPOT_MARKET_INDEX}; +use crate::{load, THIRTEEN_DAY}; +use anchor_lang::solana_program::sysvar::instructions; +use anchor_spl::associated_token::AssociatedToken; +use borsh::{BorshDeserialize, BorshSerialize}; + + +#[access_control( + withdraw_not_paused(&ctx.accounts.state) +)] +pub fn handle_swap_pool( + ctx: Context, + market_index: u16, + amount: u64, + reduce_only: bool, +) -> anchor_lang::Result<()> { + + // take two spot market accounts and swap their fee/revenue pool + // limits for what can be imposed on both revenue/fee pool size + // quote fee pool must stay above $250 + // non-quote market revenue pool must stay above 5% emission for IF stakers + // fill price is oracle +/- confidence + fee + + let in_fee_pool = + let out_revenue_pool = + + + + let spot_reserve_record = SwapSpotPoolRecord { + ts: now, + authority: user.authority, + }; + emit!(spot_reserve_record); +} \ No newline at end of file diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 954548349..e760bc75e 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -3718,3 +3718,146 @@ mod select_margin_type_for_perp_maker { assert_eq!(margin_type, MarginRequirementType::Maintenance); } } + +mod fallback_price_logic { + use crate::math::constants::{ + AMM_RESERVE_PRECISION, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_I64, + }; + use crate::state::oracle::HistoricalOracleData; + use crate::state::perp_market::{PerpMarket, AMM}; + use crate::{MarketStatus, PositionDirection}; + + #[test] + fn test() { + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + min_order_size: 1000, + // oracle: oracle_price_key, + base_spread: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + // fallback are wide from oracle cause twaps arent set on amm + let result = market + .amm + .get_fallback_price( + &PositionDirection::Long, + 0, + 2012 * PRICE_PRECISION_I64 / 100, + 0, + ) + .unwrap(); + assert_eq!(result, 22132000); + + let result = market + .amm + .get_fallback_price( + &PositionDirection::Short, + 0, + 2012 * PRICE_PRECISION_I64 / 100, + 0, + ) + .unwrap(); + assert_eq!(result, 0); + + // make non-zero bid/ask twaps + market.amm.last_ask_price_twap = (101 * PRICE_PRECISION) as u64; + market.amm.last_bid_price_twap = (99 * PRICE_PRECISION) as u64; + + // fallback is offset from oracle + let result = market + .amm + .get_fallback_price( + &PositionDirection::Long, + 0, + 2012 * PRICE_PRECISION_I64 / 100, + 0, + ) + .unwrap(); + assert_eq!(result, 23132000); + + let result = market + .amm + .get_fallback_price( + &PositionDirection::Short, + 0, + 2012 * PRICE_PRECISION_I64 / 100, + 0, + ) + .unwrap(); + assert_eq!(result, 17108000); + + // ignores current oracle price and just prices fallback based on amm liquidity + let result = market + .amm + .get_fallback_price( + &PositionDirection::Long, + 1000000000, + 2012 * PRICE_PRECISION_I64 / 100, + 0, + ) + .unwrap(); + assert_eq!(result, 101000000); + + let result = market + .amm + .get_fallback_price( + &PositionDirection::Short, + 1000000000, + 2012 * PRICE_PRECISION_I64 / 100, + 0, + ) + .unwrap(); + assert_eq!(result, 99000000); + + // ignores current oracle price and just prices fallback based on amm liquidity + // tighter when seconds til expiry is long + let result = market + .amm + .get_fallback_price( + &PositionDirection::Long, + 1000000000, + 2012 * PRICE_PRECISION_I64 / 100, + 100, + ) + .unwrap(); + assert_eq!(result, 100500000); + + let result = market + .amm + .get_fallback_price( + &PositionDirection::Short, + 1000000000, + 2012 * PRICE_PRECISION_I64 / 100, + 100, + ) + .unwrap(); + assert_eq!(result, 99500000); + } +} diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 148246ac8..da9a56014 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -771,43 +771,56 @@ impl Default for AMM { } impl AMM { - pub fn get_fallback_price(self, + pub fn get_fallback_price( + self, direction: &PositionDirection, - amm_available_liquidity: u64, - oracle_price: i64, + amm_available_liquidity: u64, + oracle_price: i64, seconds_til_order_expiry: i64, - ) -> DriftResult { + ) -> DriftResult { // PRICE_PRECISION if direction.eq(&PositionDirection::Long) { - // pick amm ask + buffer if theres liquidity + // pick amm ask + buffer if theres liquidity // otherwise be aggressive vs oracle + 1hr premium if amm_available_liquidity >= self.min_order_size { let reserve_price = self.reserve_price()?; let amm_ask_price: i64 = self.ask_price(reserve_price)?.cast()?; amm_ask_price - .safe_add(amm_ask_price / (seconds_til_order_expiry * 20).clamp(100, 200)) + .safe_add(amm_ask_price / (seconds_til_order_expiry * 20).clamp(100, 200))? + .cast::() } else { - oracle_price.safe_add(self.last_ask_price_twap.cast::()? - .safe_sub(self.historical_oracle_data.last_oracle_price_twap)? - .max(0) - )?.safe_add(oracle_price / (seconds_til_order_expiry * 2).clamp(10, 50)) + oracle_price + .safe_add( + self.last_ask_price_twap + .cast::()? + .safe_sub(self.historical_oracle_data.last_oracle_price_twap)? + .max(0), + )? + .safe_add(oracle_price / (seconds_til_order_expiry * 2).clamp(10, 50))? + .cast::() } } else { - // pick amm bid - buffer if theres liquidity + // pick amm bid - buffer if theres liquidity // otherwise be aggressive vs oracle + 1hr bid premium if amm_available_liquidity >= self.min_order_size { let reserve_price = self.reserve_price()?; let amm_bid_price: i64 = self.bid_price(reserve_price)?.cast()?; amm_bid_price - .safe_sub(amm_bid_price / (seconds_til_order_expiry * 20).clamp(100, 200)) + .safe_sub(amm_bid_price / (seconds_til_order_expiry * 20).clamp(100, 200))? + .cast::() } else { - oracle_price.safe_add(self.last_bid_price_twap.cast::()? - .safe_sub(self.historical_oracle_data.last_oracle_price_twap)? - .min(0) - )?.safe_add(oracle_price / (seconds_til_order_expiry * 2).clamp(10, 50)) + oracle_price + .safe_add( + self.last_bid_price_twap + .cast::()? + .safe_sub(self.historical_oracle_data.last_oracle_price_twap)? + .min(0), + )? + .safe_sub(oracle_price / (seconds_til_order_expiry * 2).clamp(10, 50))? + .max(0) + .cast::() } } - } pub fn get_protocol_owned_position(self) -> DriftResult { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index c5135bbd1..7c8f9c0af 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1000,7 +1000,7 @@ pub enum AssetType { impl Order { pub fn seconds_til_expiry(self, now: i64) -> i64 { - self.max_ts - now + (self.max_ts - now).max(0) } pub fn has_oracle_price_offset(self) -> bool { diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 6fa030eb1..74a645470 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -3274,8 +3274,9 @@ export class DriftClient { subAccountId?: number ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.SPOT }); - const userAccountPublicKey = - await this.getUserAccountPublicKey(subAccountId); + const userAccountPublicKey = await this.getUserAccountPublicKey( + subAccountId + ); const remainingAccounts = this.getRemainingAccounts({ userAccounts: [this.getUserAccount(subAccountId)], @@ -5785,8 +5786,9 @@ export class DriftClient { } if (initializeStakeAccount) { - const initializeIx = - await this.getInitializeInsuranceFundStakeIx(marketIndex); + const initializeIx = await this.getInitializeInsuranceFundStakeIx( + marketIndex + ); addIfStakeIxs.push(initializeIx); } From 671b31d73ea66197dad8195a1bda779a72082ae1 Mon Sep 17 00:00:00 2001 From: 0xbigz <83473873+0xbigz@users.noreply.github.com> Date: Fri, 29 Dec 2023 15:58:50 -0500 Subject: [PATCH 3/3] rm excess code --- programs/drift/src/instructions/spot.rs | 100 ------------------------ 1 file changed, 100 deletions(-) delete mode 100644 programs/drift/src/instructions/spot.rs diff --git a/programs/drift/src/instructions/spot.rs b/programs/drift/src/instructions/spot.rs deleted file mode 100644 index 0380ceb29..000000000 --- a/programs/drift/src/instructions/spot.rs +++ /dev/null @@ -1,100 +0,0 @@ - -use anchor_lang::prelude::*; -use anchor_lang::Discriminator; -use anchor_spl::token::{Token, TokenAccount}; -use solana_program::program::invoke; -use solana_program::system_instruction::transfer; - -use crate::controller::orders::{cancel_orders, ModifyOrderId}; -use crate::controller::position::PositionDirection; -use crate::controller::spot_balance::update_revenue_pool_balances; -use crate::controller::spot_position::{ - charge_withdraw_fee, update_spot_balances_and_cumulative_deposits, - update_spot_balances_and_cumulative_deposits_with_limits, -}; -use crate::error::ErrorCode; -use crate::ids::{ - jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, marinade_mainnet, serum_program, -}; -use crate::instructions::constraints::*; -use crate::instructions::optional_accounts::{ - get_maker_and_maker_stats, get_referrer_and_referrer_stats, get_whitelist_token, load_maps, - AccountMaps, -}; -use crate::instructions::SpotFulfillmentType; -use crate::load_mut; -use crate::math::casting::Cast; -use crate::math::liquidation::is_user_being_liquidated; -use crate::math::margin::{ - calculate_max_withdrawable_amount, meets_initial_margin_requirement, - meets_withdraw_margin_requirement, validate_spot_margin_trading, MarginRequirementType, -}; -use crate::math::safe_math::SafeMath; -use crate::math::spot_balance::get_token_value; -use crate::math::spot_swap; -use crate::math::spot_swap::{calculate_swap_price, validate_price_bands_for_swap}; -use crate::math_error; -use crate::print_error; -use crate::safe_decrement; -use crate::safe_increment; -use crate::state::events::{ - SwapSpotPoolRecord, -}; -use crate::state::fill_mode::FillMode; -use crate::state::fulfillment_params::drift::MatchFulfillmentParams; -use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; -use crate::state::fulfillment_params::serum::SerumFulfillmentParams; -use crate::state::oracle::StrictOraclePrice; -use crate::state::order_params::{ - ModifyOrderParams, OrderParams, PlaceOrderOptions, PostOnlyParam, -}; -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; -use crate::state::spot_market::SpotBalanceType; -use crate::state::spot_market::SpotMarket; -use crate::state::spot_market_map::{ - get_writable_spot_market_set, get_writable_spot_market_set_from_many, -}; -use crate::state::state::State; -use crate::state::traits::Size; -use crate::state::user::{MarketType, OrderType, ReferrerName, User, UserStats}; -use crate::state::user_map::load_user_maps; -use crate::validate; -use crate::validation::user::validate_user_deletion; -use crate::validation::whitelist::validate_whitelist_token; -use crate::{controller, math}; -use crate::{get_then_update_id, QUOTE_SPOT_MARKET_INDEX}; -use crate::{load, THIRTEEN_DAY}; -use anchor_lang::solana_program::sysvar::instructions; -use anchor_spl::associated_token::AssociatedToken; -use borsh::{BorshDeserialize, BorshSerialize}; - - -#[access_control( - withdraw_not_paused(&ctx.accounts.state) -)] -pub fn handle_swap_pool( - ctx: Context, - market_index: u16, - amount: u64, - reduce_only: bool, -) -> anchor_lang::Result<()> { - - // take two spot market accounts and swap their fee/revenue pool - // limits for what can be imposed on both revenue/fee pool size - // quote fee pool must stay above $250 - // non-quote market revenue pool must stay above 5% emission for IF stakers - // fill price is oracle +/- confidence + fee - - let in_fee_pool = - let out_revenue_pool = - - - - let spot_reserve_record = SwapSpotPoolRecord { - ts: now, - authority: user.authority, - }; - emit!(spot_reserve_record); -} \ No newline at end of file