From af7841671f8deee49bc1c9046bcfbfd961bc5ad9 Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Sun, 3 Nov 2024 21:40:41 +0400 Subject: [PATCH 01/11] get liquidation price --- programs/flash-compute/src/lib.rs | 149 ++++++++++++++++++++++++++++++ programs/flash-read/src/states.rs | 52 +++++++++++ 2 files changed, 201 insertions(+) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index 22cae73..baa4c4c 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -160,6 +160,100 @@ pub mod flash_compute { Ok((sflp_price_usd, flp_price)) } + + pub fn get_liquidation_price( + ctx: Context, + ) -> Result { + let position = &ctx.accounts.position; + let pool = &ctx.accounts.pool; + let market = &ctx.accounts.market; + let target_custody = &ctx.accounts.target_custody; + let collateral_custody = &ctx.accounts.collateral_custody; + + let pyth_price = Account::::try_from(&ctx.accounts.collateral_oracle_account)?; + + let collateral_price = OraclePrice { + price: pyth_price.price_message.price as u64, + exponent: pyth_price.price_message.exponent as i32, + }; + + let liabilities_usd = math::checked_add( + math::checked_add( + pool.get_fee_amount(position.size_usd, target_custody.fees.close_position)?, + collateral_custody.get_lock_fee_usd(position, solana_program::sysvar::clock::Clock::get()?.unix_timestamp)? + )?, + math::checked_add( + position.unsettled_fees_usd, + math::checked_as_u64(math::checked_div( + math::checked_mul(position.size_usd as u128, Perpetuals::BPS_POWER)?, + target_custody.pricing.max_leverage as u128, + )?)? + )?, + )?; + + if market.correlation && market.side == Side::Long { + // For Correlated Long Markets, if notional size value is assumed to correspond to liabilities then current size vlaue corresponds to assets + // Liq Price = (size_usd + liabilities_usd) / (size_amount + collateral_amount) subject to constraints + let liq_price = OraclePrice::new( + math::checked_as_u64(math::checked_div( + math::checked_mul( + math::checked_add(position.size_usd, liabilities_usd)? as u128, + math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, // USD to Rate decimals for granularity + )?, + math::checked_add(position.size_amount, position.collateral_amount)? as u128, + )?)?, + -(Perpetuals::RATE_DECIMALS as i32), + ); + Ok(liq_price.scale_to_exponent(position.entry_price.exponent)?) + // } else if market.correlation && market.side == Side::Short { + // Invalid combination of market side and correlation as anti-correlated short markets do not exist + } else { + // For uncorrelated markets, assume assets_usd corresponding to collateral value to be constant + let assets_usd = collateral_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)?; + + if assets_usd >= liabilities_usd { + // Position is nominally solvent and shall be liqudaited in case of loss + let mut price_diff_loss = OraclePrice::new( + math::checked_as_u64(math::checked_div( + math::checked_mul( + math::checked_sub(assets_usd, liabilities_usd)? as u128, + math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, + )?, + position.size_amount as u128, + )?)?, + -(Perpetuals::RATE_DECIMALS as i32), + ).scale_to_exponent(position.entry_price.exponent)?; + if market.side == Side::Long { + // For Longs, loss implies price drop + price_diff_loss.price = position.entry_price.price.saturating_sub(price_diff_loss.price); + } else { + // For Shorts, loss implies price rise + price_diff_loss.price = position.entry_price.price.saturating_add(price_diff_loss.price); + } + Ok(price_diff_loss) + } else { + // Position is nominally insolvent and shall be liqudaited with profit to cover outstanding liabilities + let mut price_diff_profit = OraclePrice::new( + math::checked_as_u64(math::checked_div( + math::checked_mul( + math::checked_sub(liabilities_usd, assets_usd)? as u128, + math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, + )?, + position.size_amount as u128, + )?)?, + -(Perpetuals::RATE_DECIMALS as i32), + ).scale_to_exponent(position.entry_price.exponent)?; + if market.side == Side::Long { + // For Longs, profit implies price rise + price_diff_profit.price = position.entry_price.price.saturating_add(price_diff_profit.price); + } else { + // For Shorts, profit implies price drop + price_diff_profit.price = position.entry_price.price.saturating_sub(price_diff_profit.price); + } + Ok(price_diff_profit) + } + } + } } #[derive(Accounts)] @@ -192,3 +286,58 @@ pub struct GetPoolTokenPrices<'info> { // pool.custodies.len() custody oracles (read-only, unsigned) // pool.markets.len() market accounts (read-only, unsigned) } + +#[derive(Accounts)] +pub struct GetLiquidationPrice<'info> { + #[account( + seeds = [b"perpetuals"], + bump = perpetuals.perpetuals_bump + )] + pub perpetuals: Box>, + + #[account( + seeds = [b"pool", + pool.name.as_bytes()], + bump = pool.bump + )] + pub pool: Box>, + + #[account( + seeds = [b"position", + position.owner.as_ref(), + market.key().as_ref()], + bump = position.bump + )] + pub position: Box>, + + #[account( + seeds = [b"market", + target_custody.key().as_ref(), + collateral_custody.key().as_ref(), + &[market.side as u8]], + bump = market.bump + )] + pub market: Box>, + + #[account( + seeds = [b"custody", + pool.key().as_ref(), + target_custody.mint.key().as_ref()], + bump = target_custody.bump + )] + pub target_custody: Box>, + + #[account( + seeds = [b"custody", + pool.key().as_ref(), + collateral_custody.mint.key().as_ref()], + bump = collateral_custody.bump, + )] + pub collateral_custody: Box>, + + /// CHECK: oracle account for the collateral token + #[account( + constraint = collateral_oracle_account.key() == collateral_custody.oracle.ext_oracle_account + )] + pub collateral_oracle_account: AccountInfo<'info> +} diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index bf07b6a..148d2d3 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -210,6 +210,16 @@ pub struct Pool { impl Pool { pub const LEN: usize = 8 + 64 + std::mem::size_of::(); + + pub fn get_fee_amount(&self, fee: u64, amount: u64) -> Result { + if fee == 0 || amount == 0 { + return Ok(0); + } + math::checked_as_u64(math::checked_ceil_div( + math::checked_mul(amount as u128, fee as u128)?, + Perpetuals::RATE_POWER, + )?) + } } #[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug)] @@ -344,6 +354,48 @@ pub struct Custody { pub size_factor_for_spread: u8, } +impl Custody { + + pub const LEN: usize = 8 + std::mem::size_of::(); + + pub fn get_lock_fee_usd(&self, position: &Position, curtime: i64) -> Result { + if position.locked_usd == 0 || self.is_virtual { + return Ok(0); + } + + let cumulative_lock_fee = self.get_cumulative_lock_fee(curtime)?; + + let position_lock_fee = if cumulative_lock_fee > position.cumulative_lock_fee_snapshot { + math::checked_sub(cumulative_lock_fee, position.cumulative_lock_fee_snapshot)? + } else { + return Ok(0); + }; + + math::checked_as_u64(math::checked_div( + math::checked_mul(position_lock_fee, position.locked_usd as u128)?, + Perpetuals::RATE_POWER, + )?) + } + + pub fn get_cumulative_lock_fee(&self, curtime: i64) -> Result { + if curtime > self.borrow_rate_state.last_update { + let cumulative_lock_fee = math::checked_ceil_div( + math::checked_mul( + math::checked_sub(curtime, self.borrow_rate_state.last_update)? as u128, + self.borrow_rate_state.current_rate as u128, + )?, + 3600, + )?; + math::checked_add( + self.borrow_rate_state.cumulative_lock_fee, + cumulative_lock_fee, + ) + } else { + Ok(self.borrow_rate_state.cumulative_lock_fee) + } + } +} + #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] pub struct MarketPermissions { pub allow_open_position: bool, From 62da8b547d99e9660ceff146d7dc8850f9b7bc09 Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Wed, 11 Dec 2024 20:13:28 +0400 Subject: [PATCH 02/11] add spread calculations and fix pricing --- programs/flash-compute/src/lib.rs | 2 +- programs/flash-read/src/states.rs | 32 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index baa4c4c..5567ad9 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -173,7 +173,7 @@ pub mod flash_compute { let pyth_price = Account::::try_from(&ctx.accounts.collateral_oracle_account)?; let collateral_price = OraclePrice { - price: pyth_price.price_message.price as u64, + price: math::checked_sub(pyth_price.price_message.price as u64, pyth_price.price_message.conf)?, exponent: pyth_price.price_message.exponent as i32, }; diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index 148d2d3..6ff4d61 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -394,6 +394,38 @@ impl Custody { Ok(self.borrow_rate_state.cumulative_lock_fee) } } + + pub fn get_trade_spread( + &self, + size_usd: u64, + ) -> Result { + + if self.pricing.trade_spread_max == 0 { + return Ok(0); + } + + let slope = math::checked_div( + math::checked_mul( + math::checked_sub( + self.pricing.trade_spread_max, + self.pricing.trade_spread_min + )?, + (Perpetuals::RATE_POWER + Perpetuals::BPS_POWER) as u64 + )?, + self.pricing.max_position_locked_usd, + )?; + + Ok( + math::checked_add( + self.pricing.trade_spread_min, + math::checked_div( + math::checked_mul(slope, size_usd)?, + (Perpetuals::RATE_POWER + Perpetuals::BPS_POWER) as u64 + )? + )? + ) + } + } #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] From 489d9815f0223bc42caacfffa976d33be7919a7e Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Wed, 18 Dec 2024 14:40:39 +0400 Subject: [PATCH 03/11] sync state --- programs/flash-read/src/states.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index 6ff4d61..d257ffe 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -352,6 +352,11 @@ pub struct Custody { pub token_account_bump: u8, pub size_factor_for_spread: u8, + pub null: u8, + pub reserved_amount: u64, // TODO: Requires Realloc and shoulkd also be checked along assets + pub min_reserve_usd: u64, + pub limit_price_buffer_bps: u64, // BPS_DECIMALS + pub padding: [u8; 32], } impl Custody { From 9886210097504006b84fca7b2ccd0ae975165d8e Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Mon, 17 Feb 2025 15:57:24 +0530 Subject: [PATCH 04/11] get divergence --- programs/flash-read/src/states.rs | 59 +++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index d257ffe..b609201 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -2,6 +2,9 @@ use anchor_lang::prelude::*; use core::cmp::Ordering; use crate::{error::CompError, math}; +const ORACLE_EXPONENT_SCALE: i32 = -9; +const ORACLE_PRICE_SCALE: u64 = 1_000_000_000; +const ORACLE_MAX_PRICE: u64 = (1 << 28) - 1; // 268435455 #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] pub struct Permissions { @@ -122,6 +125,22 @@ impl OraclePrice { ) } + /// Returns price with mantissa normalized to be less than ORACLE_MAX_PRICE + pub fn normalize(&self) -> Result { + let mut p = self.price; + let mut e = self.exponent; + + while p > ORACLE_MAX_PRICE { + p = math::checked_div(p, 10)?; + e = math::checked_add(e, 1)?; + } + + Ok(OraclePrice { + price: p, + exponent: e, + }) + } + pub fn checked_sub(&self, other: &OraclePrice) -> Result { require!( self.exponent == other.exponent, @@ -133,6 +152,22 @@ impl OraclePrice { )) } + pub fn checked_div(&self, other: &OraclePrice) -> Result { + let base = self.normalize()?; + let other = other.normalize()?; + + Ok(OraclePrice { + price: math::checked_div( + math::checked_mul(base.price, ORACLE_PRICE_SCALE)?, + other.price, + )?, + exponent: math::checked_sub( + math::checked_add(base.exponent, ORACLE_EXPONENT_SCALE)?, + other.exponent, + )?, + }) + } + pub fn scale_to_exponent(&self, target_exponent: i32) -> Result { if target_exponent == self.exponent { return Ok(*self); @@ -150,6 +185,16 @@ impl OraclePrice { }) } } + + fn get_divergence(price: OraclePrice, reference: OraclePrice) -> Result { + + let factor = if price > reference { + price.checked_sub(&reference)?.checked_div(&reference)? + } else { + reference.checked_sub(&price)?.checked_div(&reference)? + }; + Ok((factor.scale_to_exponent(-(Perpetuals::BPS_DECIMALS as i32))?.price) as u64) + } } #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] @@ -159,7 +204,8 @@ pub struct OracleParams { pub oracle_type: OracleType, pub max_divergence_bps: u64, pub max_conf_bps: u64, - pub max_price_age_sec: u64, + pub max_price_age_sec: u32, + pub max_backup_age_sec: u32, } #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] @@ -255,7 +301,7 @@ pub struct Fees { pub remove_liquidity: RatioFees, pub open_position: u64, pub close_position: u64, - pub remove_collateral: u64, + pub volatility: u64, } #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] @@ -353,7 +399,7 @@ pub struct Custody { pub size_factor_for_spread: u8, pub null: u8, - pub reserved_amount: u64, // TODO: Requires Realloc and shoulkd also be checked along assets + pub reserved_amount: u64, pub min_reserve_usd: u64, pub limit_price_buffer_bps: u64, // BPS_DECIMALS pub padding: [u8; 32], @@ -551,3 +597,10 @@ pub struct StakeStats { pub pending_deactivation: u64, pub deactivated_amount: u64, } + +#[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] +pub struct NewPositionPricesAndFee { + pub entry_price: OraclePrice, + pub entry_fee_amount: u64, + pub vb_fee_amount: u64, +} From e2dc349f0b1fa86d9b6c50807ffdf887edd5924f Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Mon, 24 Feb 2025 17:25:28 +0530 Subject: [PATCH 05/11] oracle price helpers --- programs/flash-read/src/error.rs | 2 + programs/flash-read/src/states.rs | 174 ++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/programs/flash-read/src/error.rs b/programs/flash-read/src/error.rs index 53ddebe..48cccaf 100644 --- a/programs/flash-read/src/error.rs +++ b/programs/flash-read/src/error.rs @@ -8,4 +8,6 @@ pub enum CompError { MathOverflow, #[msg("Exponent mismatch in arithmetic operation")] ExponentMismatch, + #[msg("Invalid oracle price")] + InvalidOraclePrice, } diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index b609201..c5ddea9 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -76,6 +76,16 @@ impl Default for OracleType { } } +#[account] +#[derive(Copy, Default, Debug)] +pub struct CustomOracle { + pub price: u64, + pub expo: i32, + pub conf: u64, + pub ema: u64, + pub publish_time: i64, +} + #[derive(Copy, Clone, Eq, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] pub struct OraclePrice { pub price: u64, @@ -125,6 +135,20 @@ impl OraclePrice { ) } + // Converts USD amount with implied USD_DECIMALS decimals to token amount + pub fn get_token_amount(&self, asset_amount_usd: u64, token_decimals: u8) -> Result { + if asset_amount_usd == 0 || self.price == 0 { + return Ok(0); + } + math::checked_decimal_div( + asset_amount_usd, + -(Perpetuals::USD_DECIMALS as i32), + self.price, + self.exponent, + -(token_decimals as i32), + ) + } + /// Returns price with mantissa normalized to be less than ORACLE_MAX_PRICE pub fn normalize(&self) -> Result { let mut p = self.price; @@ -195,6 +219,81 @@ impl OraclePrice { }; Ok((factor.scale_to_exponent(-(Perpetuals::BPS_DECIMALS as i32))?.price) as u64) } + + fn get_int_oracle_price( + custom_price_info: &AccountInfo, + ) -> Result<(OraclePrice, OraclePrice, u64, i64)> { + let oracle_acc = Account::::try_from(custom_price_info)?; + Ok(( + OraclePrice::new(oracle_acc.price, oracle_acc.expo), + OraclePrice::new(oracle_acc.ema, oracle_acc.expo), + oracle_acc.conf, + oracle_acc.publish_time, + )) + } + + // Returns (min_oracle_price, max_oracle_price, volatility_flag) + pub fn fetch_from_oracle( + int_oracle_account: &AccountInfo, + oracle_params: &OracleParams, // from custody.oracle + current_time: i64, + is_stable: bool, + ) -> Result<( + OraclePrice, + OraclePrice, + bool, + )> { + let ( + oracle_price, + oracle_ema_price, + oracle_conf, + oracle_timestamp, + ) = Self::get_int_oracle_price(int_oracle_account)?; + + let price_age_sec = current_time.saturating_sub(oracle_timestamp); + if price_age_sec > oracle_params.max_price_age_sec as i64 { + return err!(CompError::InvalidOraclePrice); + } + + let divergence_bps = if is_stable { + let one_usd = OraclePrice::new( + math::checked_pow(10_u64, oracle_price.exponent.abs() as usize)?, + oracle_price.exponent, + ); + Self::get_divergence(oracle_price, one_usd)? + } else { + Self::get_divergence(oracle_price, oracle_ema_price)? + }; + + if divergence_bps < oracle_params.max_divergence_bps { + Ok(( + oracle_price, + oracle_price, + false, + )) + } else { + let conf_bps = math::checked_div( + math::checked_mul(oracle_conf as u128, Perpetuals::BPS_POWER)?, + oracle_price.price as u128, + )?; + + if conf_bps < oracle_params.max_conf_bps as u128 { + Ok(( + OraclePrice::new( + math::checked_sub(oracle_price.price, oracle_conf)?, + oracle_price.exponent, + ), + OraclePrice::new( + math::checked_add(oracle_price.price, oracle_conf)?, + oracle_price.exponent, + ), + true, + )) + } else { + err!(CompError::InvalidOraclePrice) + } + } + } } #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] @@ -266,6 +365,81 @@ impl Pool { Perpetuals::RATE_POWER, )?) } + + fn get_price( + &self, + min_price: &OraclePrice, + max_price: &OraclePrice, + side: Side, + spread: u64, + ) -> Result { + if side == Side::Long { + Ok(OraclePrice { + price: math::checked_add( + max_price.price, + math::checked_decimal_ceil_mul( + max_price.price, + max_price.exponent, + spread, + -(Perpetuals::USD_DECIMALS as i32), // Spread is in 100th of a bip so we use USD decimals + max_price.exponent, + )?, + )?, + exponent: max_price.exponent, + }) + } else { + let spread = math::checked_decimal_mul( + min_price.price, + min_price.exponent, + spread, + -(Perpetuals::USD_DECIMALS as i32), + min_price.exponent, + )?; + + let price = if spread < min_price.price { + math::checked_sub(min_price.price, spread)? + } else { + 0 + }; + + Ok(OraclePrice { + price, + exponent: min_price.exponent, + }) + } + } + + pub fn get_entry_price( + &self, + min_price: &OraclePrice, + max_price: &OraclePrice, + side: Side, + spread: u64, // from: target_custody.get_trade_spread(position.size_usd) + ) -> Result { + let price = self.get_price(min_price, max_price, side, spread)?; + Ok(price) + } + + pub fn get_exit_price( + &self, + min_price: &OraclePrice, + max_price: &OraclePrice, + side: Side, + spread: u64, // from: target_custody.get_trade_spread(position.size_usd) + ) -> Result { + let price = self.get_price( + min_price, + max_price, + if side == Side::Long { + Side::Short + } else { + Side::Long + }, + spread, + )?; + + Ok(price) + } } #[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug)] From 627d7f9a8954f7e8b3ffe8463baa6d08bc0be82d Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Wed, 12 Mar 2025 05:44:36 +0530 Subject: [PATCH 06/11] liquidation price change for FLU --- programs/flash-compute/src/lib.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index 5567ad9..62988ca 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -176,6 +176,13 @@ pub mod flash_compute { price: math::checked_sub(pyth_price.price_message.price as u64, pyth_price.price_message.conf)?, exponent: pyth_price.price_message.exponent as i32, }; + + let pyth_price = Account::::try_from(&ctx.accounts.target_oracle_account)?; + + let target_price = OraclePrice { + price: math::checked_sub(pyth_price.price_message.price as u64, pyth_price.price_message.conf)?, + exponent: pyth_price.price_message.exponent as i32, + }; let liabilities_usd = math::checked_add( math::checked_add( @@ -192,6 +199,18 @@ pub mod flash_compute { )?; if market.correlation && market.side == Side::Long { + let collateral_amount = if market.target_custody_id == market.collateral_custody_id { + position.collateral_amount + } else { + let swap_price = collateral_price.checked_div(&target_price)?; + math::checked_decimal_mul( + position.collateral_amount, + -(collateral_custody.decimals as i32), + swap_price.price, + swap_price.exponent, + -(target_custody.decimals as i32), + )? + }; // For Correlated Long Markets, if notional size value is assumed to correspond to liabilities then current size vlaue corresponds to assets // Liq Price = (size_usd + liabilities_usd) / (size_amount + collateral_amount) subject to constraints let liq_price = OraclePrice::new( @@ -200,7 +219,7 @@ pub mod flash_compute { math::checked_add(position.size_usd, liabilities_usd)? as u128, math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, // USD to Rate decimals for granularity )?, - math::checked_add(position.size_amount, position.collateral_amount)? as u128, + math::checked_add(position.size_amount, collateral_amount)? as u128, )?)?, -(Perpetuals::RATE_DECIMALS as i32), ); @@ -327,6 +346,12 @@ pub struct GetLiquidationPrice<'info> { )] pub target_custody: Box>, + /// CHECK: oracle account for the target token + #[account( + constraint = target_oracle_account.key() == target_custody.oracle.ext_oracle_account + )] + pub target_oracle_account: AccountInfo<'info>, + #[account( seeds = [b"custody", pool.key().as_ref(), From ff9e9bd072f5e9e1cee2dab78d0075e8269b76dd Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Tue, 24 Jun 2025 18:35:20 +0530 Subject: [PATCH 07/11] fix custody uid and index mismatch errors --- programs/flash-compute/src/lib.rs | 38 ++++++++++++++++--------------- programs/flash-read/src/error.rs | 2 ++ programs/flash-read/src/states.rs | 13 +++++++++-- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index 62988ca..7c1ec9a 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -64,66 +64,68 @@ pub mod flash_compute { for (idx, &market) in pool.markets.iter().enumerate() { require_keys_eq!(ctx.remaining_accounts[(pool.custodies.len() * 2) + idx].key(), market); let market = Box::new(Account::::try_from(&ctx.remaining_accounts[(pool.custodies.len() * 2) + idx])?); + let target_custody_id = pool.get_custody_id(&market.target_custody)?; + let collateral_custody_id = pool.get_custody_id(&market.collateral_custody)?; // Get the collective position against the pool let position = Box::new(market.get_collective_position()?); if market.side == Side::Short { let exit_price = OraclePrice { price: math::checked_add( - custody_details[market.target_custody_id].max_price.price, + custody_details[target_custody_id].max_price.price, math::checked_decimal_ceil_mul( - custody_details[market.target_custody_id].max_price.price, - custody_details[market.target_custody_id].max_price.exponent, - custody_details[market.target_custody_id].trade_spread_max, + custody_details[target_custody_id].max_price.price, + custody_details[target_custody_id].max_price.exponent, + custody_details[target_custody_id].trade_spread_max, -6, // Spread is in 100th of a bip - custody_details[market.target_custody_id].max_price.exponent, + custody_details[target_custody_id].max_price.exponent, )?, )?, - exponent: custody_details[market.target_custody_id].max_price.exponent, + exponent: custody_details[target_custody_id].max_price.exponent, }; pool_equity = if exit_price < position.entry_price { // Shorts are in collective profit pool_equity.saturating_sub(std::cmp::min( position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[market.collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? as u128 + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? as u128 )) } else { // Shorts are in collective loss pool_equity.checked_add(std::cmp::min( exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[market.collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? as u128 + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? as u128 )).unwrap() }; } else { let spread = math::checked_decimal_mul( - custody_details[market.target_custody_id].min_price.price, - custody_details[market.target_custody_id].min_price.exponent, - custody_details[market.target_custody_id].trade_spread_min, + custody_details[target_custody_id].min_price.price, + custody_details[target_custody_id].min_price.exponent, + custody_details[target_custody_id].trade_spread_min, -6, // Spread is in 100th of a bip - custody_details[market.target_custody_id].min_price.exponent, + custody_details[target_custody_id].min_price.exponent, )?; - let price = if spread < custody_details[market.target_custody_id].min_price.price { - math::checked_sub(custody_details[market.target_custody_id].min_price.price, spread)? + let price = if spread < custody_details[target_custody_id].min_price.price { + math::checked_sub(custody_details[target_custody_id].min_price.price, spread)? } else { 0 }; let exit_price = OraclePrice { price, - exponent: custody_details[market.target_custody_id].min_price.exponent, + exponent: custody_details[target_custody_id].min_price.exponent, }; pool_equity = if exit_price > position.entry_price { // Longs are in collective profit pool_equity.saturating_sub(std::cmp::min( exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[market.collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? as u128 + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? as u128 )) } else { // Longs are in collective loss pool_equity.checked_add(std::cmp::min( position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[market.collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? as u128 + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? as u128 )).unwrap() }; @@ -199,7 +201,7 @@ pub mod flash_compute { )?; if market.correlation && market.side == Side::Long { - let collateral_amount = if market.target_custody_id == market.collateral_custody_id { + let collateral_amount = if market.target_custody_uid == market.collateral_custody_uid { position.collateral_amount } else { let swap_price = collateral_price.checked_div(&target_price)?; diff --git a/programs/flash-read/src/error.rs b/programs/flash-read/src/error.rs index 48cccaf..ee21be5 100644 --- a/programs/flash-read/src/error.rs +++ b/programs/flash-read/src/error.rs @@ -10,4 +10,6 @@ pub enum CompError { ExponentMismatch, #[msg("Invalid oracle price")] InvalidOraclePrice, + #[msg("Custody is not supported")] + UnsupportedCustody, } diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index c5ddea9..59717b1 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -440,6 +440,13 @@ impl Pool { Ok(price) } + + pub fn get_custody_id(&self, custody: &Pubkey) -> Result { + self.custodies + .iter() + .position(|&c| c == *custody) + .ok_or_else(|| CompError::UnsupportedCustody.into()) + } } #[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug)] @@ -704,8 +711,10 @@ pub struct Market { pub permissions: MarketPermissions, pub open_interest: u64, pub collective_position: PositionStats, - pub target_custody_id: usize, - pub collateral_custody_id: usize, + pub target_custody_uid: u8, + pub padding: [u8; 7], + pub collateral_custody_uid: u8, + pub padding2: [u8; 7], pub bump: u8, } From cbfc46cbe93af2c52a759fa45d5df00c5b4fe3a5 Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Thu, 21 Aug 2025 21:40:43 +0900 Subject: [PATCH 08/11] Fix scaling for CPI compatibility --- programs/flash-compute/src/lib.rs | 20 ++++++++++---------- programs/flash-read/src/states.rs | 23 +++++++++++++++++------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index 7c1ec9a..c05cc95 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -23,7 +23,7 @@ pub mod flash_compute { ) -> Result<(u64, u64)> { let pool = &ctx.accounts.pool; let mut custody_details: Box> = Box::new(Vec::new()); - let mut pool_equity: u128 = 0; + let mut pool_equity: u64 = 0; // Computing the raw AUM of the pool for (idx, &custody) in pool.custodies.iter().enumerate() { @@ -54,7 +54,7 @@ pub mod flash_compute { let token_amount_usd = custody_details[idx].min_price.get_asset_amount_usd(custody.assets.owned, custody.decimals)?; - pool_equity = math::checked_add(pool_equity, token_amount_usd as u128)?; + pool_equity = math::checked_add(pool_equity, token_amount_usd)?; } @@ -85,14 +85,14 @@ pub mod flash_compute { pool_equity = if exit_price < position.entry_price { // Shorts are in collective profit pool_equity.saturating_sub(std::cmp::min( - position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? as u128 + position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? )) } else { // Shorts are in collective loss pool_equity.checked_add(std::cmp::min( - exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? as u128 + exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? )).unwrap() }; } else { @@ -118,14 +118,14 @@ pub mod flash_compute { pool_equity = if exit_price > position.entry_price { // Longs are in collective profit pool_equity.saturating_sub(std::cmp::min( - exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? as u128 + exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? )) } else { // Longs are in collective loss pool_equity.checked_add(std::cmp::min( - position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)? as u128, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? as u128 + position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? )).unwrap() }; diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index 59717b1..405a6aa 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -331,26 +331,37 @@ pub struct Pool { pub inception_time: i64, pub lp_mint: Pubkey, pub oracle_authority: Pubkey, - pub staked_lp_vault: Pubkey, // set in init_staking - pub reward_custody: Pubkey, // set in init_staking + pub staked_lp_vault: Pubkey, + pub reward_custody: Pubkey, pub custodies: Vec, pub ratios: Vec, pub markets: Vec, - pub max_aum_usd: u128, - pub aum_usd: u128, // For persistnace + pub max_aum_usd: u64, + pub buffer: u64, + pub raw_aum_usd: u64, + pub equity_usd: u64, pub total_staked: StakeStats, pub staking_fee_share_bps: u64, pub bump: u8, pub lp_mint_bump: u8, pub staked_lp_vault_bump: u8, pub vp_volume_factor: u8, - pub padding: [u8; 4], - pub staking_fee_boost_bps: [u64; 6], + pub unique_custody_count: u8, + pub padding: [u8; 3], + pub staking_fee_boost_bps: [u64; 6], pub compounding_mint: Pubkey, pub compounding_lp_vault: Pubkey, pub compounding_stats: CompoundingStats, pub compounding_mint_bump: u8, pub compounding_lp_vault_bump: u8, + + pub min_lp_price_usd: u64, + pub max_lp_price_usd: u64, + + pub lp_price: u64, + pub compounding_lp_price: u64, + pub last_updated_timestamp: i64, + pub padding2: [u8; 8], } impl Pool { From 19801bdd01af31d44a71344055c4904b14b66eb5 Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Wed, 26 Nov 2025 17:43:18 +0530 Subject: [PATCH 09/11] feat: margin engine v2 --- programs/flash-compute/src/lib.rs | 177 ++++++++---------------------- programs/flash-read/src/states.rs | 15 +-- 2 files changed, 50 insertions(+), 142 deletions(-) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index c05cc95..7587f04 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -12,7 +12,7 @@ declare_id!("Fcmp5ZQ1wR5swZ87aRQyHfUiHYxrfrRVhCWrV2yYA6QG"); pub const FLASH_PROGRAM: Pubkey = pubkey!("FLASH6Lo6h3iasJKWDs2F8TkW2UKf3s15C8PMGuVfgBn"); #[cfg(not(feature = "mainnet"))] -pub const FLASH_PROGRAM: Pubkey = pubkey!("FTN6rgbaaxwT8mpRuC55EFTwpHB3BwnHJ91Lqv4ZVCfW"); +pub const FLASH_PROGRAM: Pubkey = pubkey!("FTPP4jEWW1n8s2FEccwVfS9KCPjpndaswg7Nkkuz4ER4"); #[program] pub mod flash_compute { @@ -22,7 +22,7 @@ pub mod flash_compute { ctx: Context, ) -> Result<(u64, u64)> { let pool = &ctx.accounts.pool; - let mut custody_details: Box> = Box::new(Vec::new()); + let mut custody_prices: Vec = Vec::new(); let mut pool_equity: u64 = 0; // Computing the raw AUM of the pool @@ -38,22 +38,13 @@ pub mod flash_compute { let pyth_price = Account::::try_from(&ctx.remaining_accounts[oracle_idx])?; - custody_details.push(CustodyDetails { - trade_spread_min: custody.pricing.trade_spread_min, - trade_spread_max: custody.pricing.trade_spread_max, - delay_seconds: custody.pricing.delay_seconds, - min_price: OraclePrice { + custody_prices.push(OraclePrice { price: pyth_price.price_message.price as u64, exponent: pyth_price.price_message.exponent as i32, - }, - max_price: OraclePrice { - price: pyth_price.price_message.price as u64, - exponent: pyth_price.price_message.exponent as i32, - }, }); let token_amount_usd = - custody_details[idx].min_price.get_asset_amount_usd(custody.assets.owned, custody.decimals)?; + custody_prices[idx].get_asset_amount_usd(custody.assets.owned, custody.decimals)?; pool_equity = math::checked_add(pool_equity, token_amount_usd)?; } @@ -68,64 +59,35 @@ pub mod flash_compute { let collateral_custody_id = pool.get_custody_id(&market.collateral_custody)?; // Get the collective position against the pool let position = Box::new(market.get_collective_position()?); + pool_equity = pool_equity.saturating_sub(position.collateral_usd); + let exit_price = custody_prices[target_custody_id]; if market.side == Side::Short { - let exit_price = OraclePrice { - price: math::checked_add( - custody_details[target_custody_id].max_price.price, - math::checked_decimal_ceil_mul( - custody_details[target_custody_id].max_price.price, - custody_details[target_custody_id].max_price.exponent, - custody_details[target_custody_id].trade_spread_max, - -6, // Spread is in 100th of a bip - custody_details[target_custody_id].max_price.exponent, - )?, - )?, - exponent: custody_details[target_custody_id].max_price.exponent, - }; pool_equity = if exit_price < position.entry_price { // Shorts are in collective profit pool_equity.saturating_sub(std::cmp::min( position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? + custody_prices[collateral_custody_id].get_asset_amount_usd(position.locked_amount, position.locked_decimals)? )) } else { // Shorts are in collective loss pool_equity.checked_add(std::cmp::min( exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? + custody_prices[collateral_custody_id].get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? )).unwrap() }; } else { - let spread = math::checked_decimal_mul( - custody_details[target_custody_id].min_price.price, - custody_details[target_custody_id].min_price.exponent, - custody_details[target_custody_id].trade_spread_min, - -6, // Spread is in 100th of a bip - custody_details[target_custody_id].min_price.exponent, - )?; - - let price = if spread < custody_details[target_custody_id].min_price.price { - math::checked_sub(custody_details[target_custody_id].min_price.price, spread)? - } else { - 0 - }; - - let exit_price = OraclePrice { - price, - exponent: custody_details[target_custody_id].min_price.exponent, - }; pool_equity = if exit_price > position.entry_price { // Longs are in collective profit pool_equity.saturating_sub(std::cmp::min( exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.locked_amount, position.locked_decimals)? + custody_prices[collateral_custody_id].get_asset_amount_usd(position.locked_amount, position.locked_decimals)? )) } else { // Longs are in collective loss pool_equity.checked_add(std::cmp::min( position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, - custody_details[collateral_custody_id].min_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? + custody_prices[collateral_custody_id].get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? )).unwrap() }; @@ -171,20 +133,6 @@ pub mod flash_compute { let market = &ctx.accounts.market; let target_custody = &ctx.accounts.target_custody; let collateral_custody = &ctx.accounts.collateral_custody; - - let pyth_price = Account::::try_from(&ctx.accounts.collateral_oracle_account)?; - - let collateral_price = OraclePrice { - price: math::checked_sub(pyth_price.price_message.price as u64, pyth_price.price_message.conf)?, - exponent: pyth_price.price_message.exponent as i32, - }; - - let pyth_price = Account::::try_from(&ctx.accounts.target_oracle_account)?; - - let target_price = OraclePrice { - price: math::checked_sub(pyth_price.price_message.price as u64, pyth_price.price_message.conf)?, - exponent: pyth_price.price_message.exponent as i32, - }; let liabilities_usd = math::checked_add( math::checked_add( @@ -200,79 +148,46 @@ pub mod flash_compute { )?, )?; - if market.correlation && market.side == Side::Long { - let collateral_amount = if market.target_custody_uid == market.collateral_custody_uid { - position.collateral_amount + if position.collateral_usd >= liabilities_usd { + // Position is nominally solvent and shall be liqudaited in case of loss + let mut price_diff_loss = OraclePrice::new( + math::checked_as_u64(math::checked_div( + math::checked_mul( + math::checked_sub(position.collateral_usd, liabilities_usd)? as u128, + math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, + )?, + position.size_amount as u128, + )?)?, + -(Perpetuals::RATE_DECIMALS as i32), + ).scale_to_exponent(position.entry_price.exponent)?; + if market.side == Side::Long { + // For Longs, loss implies price drop + price_diff_loss.price = position.entry_price.price.saturating_sub(price_diff_loss.price); } else { - let swap_price = collateral_price.checked_div(&target_price)?; - math::checked_decimal_mul( - position.collateral_amount, - -(collateral_custody.decimals as i32), - swap_price.price, - swap_price.exponent, - -(target_custody.decimals as i32), - )? - }; - // For Correlated Long Markets, if notional size value is assumed to correspond to liabilities then current size vlaue corresponds to assets - // Liq Price = (size_usd + liabilities_usd) / (size_amount + collateral_amount) subject to constraints - let liq_price = OraclePrice::new( - math::checked_as_u64(math::checked_div( - math::checked_mul( - math::checked_add(position.size_usd, liabilities_usd)? as u128, - math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, // USD to Rate decimals for granularity - )?, - math::checked_add(position.size_amount, collateral_amount)? as u128, - )?)?, - -(Perpetuals::RATE_DECIMALS as i32), - ); - Ok(liq_price.scale_to_exponent(position.entry_price.exponent)?) - // } else if market.correlation && market.side == Side::Short { - // Invalid combination of market side and correlation as anti-correlated short markets do not exist + // For Shorts, loss implies price rise + price_diff_loss.price = position.entry_price.price.saturating_add(price_diff_loss.price); + } + Ok(price_diff_loss) } else { - // For uncorrelated markets, assume assets_usd corresponding to collateral value to be constant - let assets_usd = collateral_price.get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)?; - - if assets_usd >= liabilities_usd { - // Position is nominally solvent and shall be liqudaited in case of loss - let mut price_diff_loss = OraclePrice::new( - math::checked_as_u64(math::checked_div( - math::checked_mul( - math::checked_sub(assets_usd, liabilities_usd)? as u128, - math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, - )?, - position.size_amount as u128, - )?)?, - -(Perpetuals::RATE_DECIMALS as i32), - ).scale_to_exponent(position.entry_price.exponent)?; - if market.side == Side::Long { - // For Longs, loss implies price drop - price_diff_loss.price = position.entry_price.price.saturating_sub(price_diff_loss.price); - } else { - // For Shorts, loss implies price rise - price_diff_loss.price = position.entry_price.price.saturating_add(price_diff_loss.price); - } - Ok(price_diff_loss) + // Position is nominally insolvent and shall be liqudaited with profit to cover outstanding liabilities + let mut price_diff_profit = OraclePrice::new( + math::checked_as_u64(math::checked_div( + math::checked_mul( + math::checked_sub(liabilities_usd, position.collateral_usd)? as u128, + math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, + )?, + position.size_amount as u128, + )?)?, + -(Perpetuals::RATE_DECIMALS as i32), + ).scale_to_exponent(position.entry_price.exponent)?; + if market.side == Side::Long { + // For Longs, profit implies price rise + price_diff_profit.price = position.entry_price.price.saturating_add(price_diff_profit.price); } else { - // Position is nominally insolvent and shall be liqudaited with profit to cover outstanding liabilities - let mut price_diff_profit = OraclePrice::new( - math::checked_as_u64(math::checked_div( - math::checked_mul( - math::checked_sub(liabilities_usd, assets_usd)? as u128, - math::checked_pow(10_u128, (position.size_decimals + 3) as usize)?, - )?, - position.size_amount as u128, - )?)?, - -(Perpetuals::RATE_DECIMALS as i32), - ).scale_to_exponent(position.entry_price.exponent)?; - if market.side == Side::Long { - // For Longs, profit implies price rise - price_diff_profit.price = position.entry_price.price.saturating_add(price_diff_profit.price); - } else { - // For Shorts, profit implies price drop - price_diff_profit.price = position.entry_price.price.saturating_sub(price_diff_profit.price); - } - Ok(price_diff_profit) + // For Shorts, profit implies price drop + price_diff_profit.price = position.entry_price.price.saturating_sub(price_diff_profit.price); } + Ok(price_diff_profit) } } } diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index 405a6aa..0099495 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -361,7 +361,9 @@ pub struct Pool { pub lp_price: u64, pub compounding_lp_price: u64, pub last_updated_timestamp: i64, - pub padding2: [u8; 8], + pub fees_obligation_usd: u64, + pub rebate_obligation_usd: u64, + pub threshold_usd: u64, } impl Pool { @@ -460,15 +462,6 @@ impl Pool { } } -#[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug)] -pub struct CustodyDetails { - pub trade_spread_min: u64, - pub trade_spread_max: u64, - pub delay_seconds: i64, - pub min_price: OraclePrice, - pub max_price: OraclePrice -} - #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)] pub enum FeesMode { Fixed, @@ -773,7 +766,7 @@ pub struct Position { pub locked_usd: u64, pub collateral_amount: u64, pub collateral_usd: u64, - pub unsettled_amount: u64, // Used for position delta accounting + pub unsettled_amount: u64, pub unsettled_fees_usd: u64, pub cumulative_lock_fee_snapshot: u128, pub take_profit_price: OraclePrice, From 5b4fdffaac4b724f576dfb9348efedb5b6248302 Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Thu, 22 Jan 2026 17:09:37 +0530 Subject: [PATCH 10/11] fix: lp price calculations post v2 changes --- programs/flash-compute/src/lib.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index 7587f04..9199c9d 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -49,6 +49,8 @@ pub mod flash_compute { } + pool_equity = pool_equity.saturating_sub(math::checked_add(pool.fees_obligation_usd, pool.rebate_obligation_usd)?); + // Computing the unrealsied PnL pending against the pool @@ -61,8 +63,8 @@ pub mod flash_compute { let position = Box::new(market.get_collective_position()?); pool_equity = pool_equity.saturating_sub(position.collateral_usd); let exit_price = custody_prices[target_custody_id]; - if market.side == Side::Short { - pool_equity = if exit_price < position.entry_price { + pool_equity = if market.side == Side::Short { + if exit_price < position.entry_price { // Shorts are in collective profit pool_equity.saturating_sub(std::cmp::min( position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, @@ -72,12 +74,11 @@ pub mod flash_compute { // Shorts are in collective loss pool_equity.checked_add(std::cmp::min( exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, - custody_prices[collateral_custody_id].get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? + position.collateral_usd )).unwrap() - }; + } } else { - - pool_equity = if exit_price > position.entry_price { + if exit_price > position.entry_price { // Longs are in collective profit pool_equity.saturating_sub(std::cmp::min( exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, @@ -87,9 +88,9 @@ pub mod flash_compute { // Longs are in collective loss pool_equity.checked_add(std::cmp::min( position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, - custody_prices[collateral_custody_id].get_asset_amount_usd(position.collateral_amount, position.collateral_decimals)? + position.collateral_usd )).unwrap() - }; + } }; } From 4ad2e370d4004a92287ee154b4fc3511664a7e86 Mon Sep 17 00:00:00 2001 From: zoheb-mzs Date: Thu, 22 Jan 2026 22:37:49 +0530 Subject: [PATCH 11/11] feat: real time lp token prices --- programs/flash-compute/src/lib.rs | 139 ++++++++++++++++++++++++++++++ programs/flash-read/src/states.rs | 1 + 2 files changed, 140 insertions(+) diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index 9199c9d..32ff34e 100644 --- a/programs/flash-compute/src/lib.rs +++ b/programs/flash-compute/src/lib.rs @@ -126,6 +126,114 @@ pub mod flash_compute { Ok((sflp_price_usd, flp_price)) } + pub fn get_realtime_pool_token_prices( + ctx: Context, + ) -> Result<(u64, u64)> { + let pool = &ctx.accounts.pool; + let mut custody_prices: Vec = Vec::new(); + let mut pool_equity: u64 = 0; + + // Computing the raw AUM of the pool + for (idx, &custody) in pool.custodies.iter().enumerate() { + + require_keys_eq!(ctx.remaining_accounts[idx].key(), custody); + let custody = Box::new(Account::::try_from(&ctx.remaining_accounts[idx])?); + let oracle_idx = idx + pool.custodies.len(); + if oracle_idx >= ctx.remaining_accounts.len() { + return Err(ProgramError::NotEnoughAccountKeys.into()); + } + require_keys_eq!(ctx.remaining_accounts[oracle_idx].key(), custody.oracle.ext_oracle_account); + + let price = Account::::try_from(&ctx.remaining_accounts[oracle_idx])?; + + custody_prices.push(OraclePrice { + price: price.price as u64, + exponent: price.expo as i32, + }); + + let token_amount_usd = + custody_prices[idx].get_asset_amount_usd(custody.assets.owned, custody.decimals)?; + pool_equity = math::checked_add(pool_equity, token_amount_usd)?; + + } + + pool_equity = pool_equity.saturating_sub(math::checked_add(pool.fees_obligation_usd, pool.rebate_obligation_usd)?); + + // Computing the unrealsied PnL pending against the pool + + + for (idx, &market) in pool.markets.iter().enumerate() { + require_keys_eq!(ctx.remaining_accounts[(pool.custodies.len() * 2) + idx].key(), market); + let market = Box::new(Account::::try_from(&ctx.remaining_accounts[(pool.custodies.len() * 2) + idx])?); + let target_custody_id = pool.get_custody_id(&market.target_custody)?; + let collateral_custody_id = pool.get_custody_id(&market.collateral_custody)?; + // Get the collective position against the pool + let position = Box::new(market.get_collective_position()?); + pool_equity = pool_equity.saturating_sub(position.collateral_usd); + let exit_price = custody_prices[target_custody_id]; + pool_equity = if market.side == Side::Short { + if exit_price < position.entry_price { + // Shorts are in collective profit + pool_equity.saturating_sub(std::cmp::min( + position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + custody_prices[collateral_custody_id].get_asset_amount_usd(position.locked_amount, position.locked_decimals)? + )) + } else { + // Shorts are in collective loss + pool_equity.checked_add(std::cmp::min( + exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + position.collateral_usd + )).unwrap() + } + } else { + if exit_price > position.entry_price { + // Longs are in collective profit + pool_equity.saturating_sub(std::cmp::min( + exit_price.checked_sub(&position.entry_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + custody_prices[collateral_custody_id].get_asset_amount_usd(position.locked_amount, position.locked_decimals)? + )) + } else { + // Longs are in collective loss + pool_equity.checked_add(std::cmp::min( + position.entry_price.checked_sub(&exit_price)?.get_asset_amount_usd(position.size_amount, position.size_decimals)?, + position.collateral_usd + )).unwrap() + } + + }; + } + + let lp_supply = ctx.accounts.lp_token_mint.supply; + + let sflp_price_usd = math::checked_decimal_div( + math::checked_as_u64(pool_equity)?, + -(Perpetuals::USD_DECIMALS as i32), + lp_supply, + -(Perpetuals::LP_DECIMALS as i32), + -(Perpetuals::USD_DECIMALS as i32), + )?; + + let compounding_factor = math::checked_decimal_div( + pool.compounding_stats.active_amount, + -(Perpetuals::LP_DECIMALS as i32), + pool.compounding_stats.total_supply, + -(Perpetuals::LP_DECIMALS as i32), + -(Perpetuals::LP_DECIMALS as i32), + )?; + + let flp_price = math::checked_decimal_mul( + sflp_price_usd, + -(Perpetuals::USD_DECIMALS as i32), + compounding_factor, + -(Perpetuals::LP_DECIMALS as i32), + -(Perpetuals::USD_DECIMALS as i32), + )?; + + msg!("SFLP Price: {}, FLP Price: {}", sflp_price_usd, flp_price); + + Ok((sflp_price_usd, flp_price)) + } + pub fn get_liquidation_price( ctx: Context, ) -> Result { @@ -224,6 +332,37 @@ pub struct GetPoolTokenPrices<'info> { // pool.markets.len() market accounts (read-only, unsigned) } +#[derive(Accounts)] +pub struct GetRealtimePoolTokenPrices<'info> { + #[account( + seeds = [b"perpetuals"], + bump = perpetuals.perpetuals_bump, + seeds::program = FLASH_PROGRAM, + )] + pub perpetuals: Box>, + + #[account( + seeds = [b"pool", + pool.name.as_bytes()], + bump = pool.bump, + seeds::program = FLASH_PROGRAM, + )] + pub pool: Box>, + + #[account( + seeds = [b"lp_token_mint", + pool.key().as_ref()], + bump = pool.lp_mint_bump, + seeds::program = FLASH_PROGRAM, + )] + pub lp_token_mint: Box>, + + // remaining accounts: + // pool.custodies.len() custody accounts (read-only, unsigned) + // pool.custodies.len() oracles accounts corresponding to custody.int_oracle_accounts (read-only, unsigned) + // pool.markets.len() market accounts (read-only, unsigned) +} + #[derive(Accounts)] pub struct GetLiquidationPrice<'info> { #[account( diff --git a/programs/flash-read/src/states.rs b/programs/flash-read/src/states.rs index 0099495..74d2c95 100644 --- a/programs/flash-read/src/states.rs +++ b/programs/flash-read/src/states.rs @@ -84,6 +84,7 @@ pub struct CustomOracle { pub conf: u64, pub ema: u64, pub publish_time: i64, + pub ext_oracle_account: Pubkey, } #[derive(Copy, Clone, Eq, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)]