diff --git a/programs/flash-compute/src/lib.rs b/programs/flash-compute/src/lib.rs index 22cae73..32ff34e 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,8 +22,8 @@ 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 pool_equity: u128 = 0; + 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() { @@ -38,94 +38,167 @@ 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)?; - pool_equity = math::checked_add(pool_equity, token_amount_usd as u128)?; + 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()?); - if market.side == Side::Short { - let exit_price = OraclePrice { - price: math::checked_add( - custody_details[market.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, - -6, // Spread is in 100th of a bip - custody_details[market.target_custody_id].max_price.exponent, - )?, - )?, - exponent: custody_details[market.target_custody_id].max_price.exponent, - }; - pool_equity = if exit_price < position.entry_price { + 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)? as u128, - custody_details[market.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_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)? as u128, - custody_details[market.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)?, + position.collateral_usd )).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, - -6, // Spread is in 100th of a bip - custody_details[market.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)? + 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 { - 0 - }; + // 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_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 exit_price = OraclePrice { - price, - exponent: custody_details[market.target_custody_id].min_price.exponent, - }; + let price = Account::::try_from(&ctx.remaining_accounts[oracle_idx])?; - pool_equity = if exit_price > position.entry_price { + 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)? as u128, - custody_details[market.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_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)? as u128, - custody_details[market.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)?, + position.collateral_usd )).unwrap() - }; + } }; } @@ -160,6 +233,72 @@ 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 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 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 { + // 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, 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 { + // 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 +331,95 @@ pub struct GetPoolTokenPrices<'info> { // pool.custodies.len() custody oracles (read-only, unsigned) // 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( + 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>, + + /// 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(), + 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/error.rs b/programs/flash-read/src/error.rs index 53ddebe..ee21be5 100644 --- a/programs/flash-read/src/error.rs +++ b/programs/flash-read/src/error.rs @@ -8,4 +8,8 @@ pub enum CompError { MathOverflow, #[msg("Exponent mismatch in arithmetic operation")] 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 bf07b6a..74d2c95 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 { @@ -73,6 +76,17 @@ 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, + pub ext_oracle_account: Pubkey, +} + #[derive(Copy, Clone, Eq, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] pub struct OraclePrice { pub price: u64, @@ -122,6 +136,36 @@ 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; + 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 +177,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 +210,91 @@ 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) + } + + 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)] @@ -159,7 +304,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)] @@ -186,39 +332,135 @@ 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 fees_obligation_usd: u64, + pub rebate_obligation_usd: u64, + pub threshold_usd: u64, } impl Pool { pub const LEN: usize = 8 + 64 + std::mem::size_of::(); -} -#[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 + 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, + )?) + } + + 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) + } + + pub fn get_custody_id(&self, custody: &Pubkey) -> Result { + self.custodies + .iter() + .position(|&c| c == *custody) + .ok_or_else(|| CompError::UnsupportedCustody.into()) + } } #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)] @@ -245,7 +487,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)] @@ -342,6 +584,85 @@ pub struct Custody { pub token_account_bump: u8, pub size_factor_for_spread: u8, + pub null: u8, + pub reserved_amount: u64, + pub min_reserve_usd: u64, + pub limit_price_buffer_bps: u64, // BPS_DECIMALS + pub padding: [u8; 32], +} + +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) + } + } + + 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)] @@ -395,8 +716,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, } @@ -444,7 +767,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, @@ -462,3 +785,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, +}