diff --git a/.vscode/settings.json b/.vscode/settings.json index 4b3b917..799a8ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ - "sablier" + "bytemuck", + "sablier", + "Zeroable" ] } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 5d2b70f..294eafa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub use { std::str::FromStr, }; +pub mod liquidation_price; pub mod math; pub mod pda; pub mod types; @@ -39,23 +40,6 @@ pub mod main_pool { pub static WBTC_CUSTODY_ID: Pubkey = pubkey!("GFu3qS22mo6bAjg4Lr5R7L8pPgHq6GvbjJPKEHkbbs2c"); } -// BPS -pub const BPS_DECIMALS: u8 = 4; -pub const BPS_POWER: u128 = 10u64.pow(BPS_DECIMALS as u32) as u128; -// RATE -pub const RATE_POWER: u128 = 10u64.pow(RATE_DECIMALS as u32) as u128; -pub const RATE_DECIMALS: u8 = 9; -// -// Warning: A low price decimals means a possible loss of precision when interpreting pyth prices -// Look at the function scale_to_exponent -pub const PRICE_DECIMALS: u8 = 10; -// -pub const USD_DECIMALS: u8 = 6; -pub const LP_DECIMALS: u8 = USD_DECIMALS; -pub const LM_DECIMALS: u8 = USD_DECIMALS; - -pub const GOVERNANCE_SHADOW_TOKEN_DECIMALS: u8 = USD_DECIMALS; - #[program] mod adrena_abi { #![allow(dead_code)] diff --git a/src/liquidation_price.rs b/src/liquidation_price.rs new file mode 100644 index 0000000..17185e0 --- /dev/null +++ b/src/liquidation_price.rs @@ -0,0 +1,112 @@ +use { + crate::{ + math, + types::{Custody, Position}, + Cortex, Side, + }, + anyhow::Result, +}; + +pub fn get_liquidation_price( + position: &Position, + custody: &Custody, + collateral_custody: &Custody, + current_time: i64, +) -> Result { + // liq_price = pos_price +- (collateral + unreal_profit - unreal_loss - exit_fee - interest - size/max_leverage) * pos_price / size + + if position.size_usd == 0 || position.price == 0 { + return Ok(0); + } + + let total_unrealized_interest_usd = collateral_custody + .get_interest_amount_usd(position, current_time)? + + position.unrealized_interest_usd; + let unrealized_loss_usd = position.liquidation_fee_usd + total_unrealized_interest_usd; + + let mut max_loss_usd = math::checked_as_u64( + (position.size_usd as u128 * Cortex::BPS_POWER) / custody.pricing.max_leverage as u128, + )?; + + max_loss_usd += unrealized_loss_usd; + + let margin_usd = position.collateral_usd; + + let max_price_diff = if max_loss_usd >= margin_usd { + max_loss_usd - margin_usd + } else { + margin_usd - max_loss_usd + }; + + let max_price_diff = math::scale_to_exponent( + max_price_diff, + -(Cortex::USD_DECIMALS as i32), + -(Cortex::PRICE_DECIMALS as i32), + )?; + + let position_size_usd = math::scale_to_exponent( + position.size_usd, + -(Cortex::USD_DECIMALS as i32), + -(Cortex::PRICE_DECIMALS as i32), + )?; + + let max_price_diff = math::checked_as_u64( + (max_price_diff as u128 * position.price as u128) / position_size_usd as u128, + )?; + + if position.get_side() == Side::Long { + if max_loss_usd >= margin_usd { + Ok(position.price + max_price_diff) + } else if position.price > max_price_diff { + Ok(position.price - max_price_diff) + } else { + Ok(0) + } + } else if max_loss_usd >= margin_usd { + if position.price > max_price_diff { + Ok(position.price - max_price_diff) + } else { + Ok(0) + } + } else { + Ok(position.price + max_price_diff) + } +} + +// Returns the interest amount that has accrued since the last position cumulative interest snapshot update +pub fn get_interest_amount_usd( + custody: &Custody, + position: &Position, + current_time: i64, +) -> Result { + if position.borrow_size_usd == 0 { + return Ok(0); + } + + let cumulative_interest = get_cumulative_interest(custody, current_time)?; + + let position_interest = if cumulative_interest > position.cumulative_interest_snapshot.to_u128() + { + cumulative_interest - position.cumulative_interest_snapshot.to_u128() + } else { + return Ok(0); + }; + + Ok(math::checked_as_u64( + (position_interest * position.borrow_size_usd as u128) / Cortex::RATE_POWER, + )?) +} + +pub fn get_cumulative_interest(custody: &Custody, current_time: i64) -> Result { + if current_time > custody.borrow_rate_state.last_update { + let cumulative_interest = math::checked_ceil_div( + (current_time - custody.borrow_rate_state.last_update) as u128 + * custody.borrow_rate_state.current_rate as u128, + 3_600, + )?; + + Ok(custody.borrow_rate_state.cumulative_interest.to_u128() + cumulative_interest) + } else { + Ok(custody.borrow_rate_state.cumulative_interest.to_u128()) + } +} diff --git a/src/math.rs b/src/math.rs index 080c7e9..d392c8f 100644 --- a/src/math.rs +++ b/src/math.rs @@ -1,6 +1,5 @@ use { - super::BPS_POWER, - crate::U128Split, + crate::{Cortex, U128Split}, anyhow::{anyhow, Result}, std::{ fmt::Display, @@ -409,7 +408,7 @@ pub fn scale_to_exponent(arg: u64, exponent: i32, target_exponent: i32) -> Resul let result = arg / checked_pow(10, delta as usize)?; // Accept 1bps loss of precision - if result * checked_pow(10, delta as usize)? < (arg - (arg / BPS_POWER as u64)) { + if result * checked_pow(10, delta as usize)? < (arg - (arg / Cortex::BPS_POWER as u64)) { return Err(anyhow!( "Pyth price exponent too large incurring precision loss" )); diff --git a/src/types.rs b/src/types.rs index 029ed26..b47862b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,5 @@ use { + crate::math, anchor_lang::prelude::*, bytemuck::{Pod, Zeroable}, }; @@ -141,6 +142,20 @@ pub struct Cortex { pub unique_position_automation_thread_id_counter: u64, } +impl Cortex { + // BPS + pub const BPS_DECIMALS: u8 = 4; + pub const BPS_POWER: u128 = 10u64.pow(Self::BPS_DECIMALS as u32) as u128; + // RATE + pub const RATE_POWER: u128 = 10u64.pow(Self::RATE_DECIMALS as u32) as u128; + pub const RATE_DECIMALS: u8 = 9; + pub const PRICE_DECIMALS: u8 = 10; + pub const USD_DECIMALS: u8 = 6; + pub const LP_DECIMALS: u8 = Self::USD_DECIMALS; + pub const LM_DECIMALS: u8 = Cortex::USD_DECIMALS; + pub const GOVERNANCE_SHADOW_TOKEN_DECIMALS: u8 = Cortex::USD_DECIMALS; +} + #[derive( Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug, Pod, Zeroable, )] @@ -214,6 +229,64 @@ pub struct Position { pub stop_loss_close_position_price: u64, } +impl Position { + pub const LEN: usize = 8 + std::mem::size_of::(); + + pub fn is_pending_cleanup_and_close(&self) -> bool { + self.pending_cleanup_and_close != 0 + } + + pub fn get_side(&self) -> Side { + // Consider value in the struct always good + Side::try_from(self.side).unwrap() + } + + pub fn take_profit_is_set(&self) -> bool { + self.take_profit_thread_is_set != 0 + } + + pub fn stop_loss_is_set(&self) -> bool { + self.stop_loss_thread_is_set != 0 + } + + pub fn take_profit_reached(&self, price: u64) -> bool { + if self.take_profit_limit_price == 0 { + return false; + } + + if self.get_side() == Side::Long { + price >= self.take_profit_limit_price + } else { + price <= self.take_profit_limit_price + } + } + + pub fn stop_loss_reached(&self, price: u64) -> bool { + if self.stop_loss_limit_price == 0 { + return false; + } + + if self.get_side() == Side::Long { + price <= self.stop_loss_limit_price + } else { + price >= self.stop_loss_limit_price + } + } + + pub fn stop_loss_slippage_ok(&self, price: u64) -> bool { + // 0 means no slippage + if self.stop_loss_close_position_price == 0 { + return true; + } + + if self.get_side() == Side::Long { + price >= self.stop_loss_close_position_price + } else { + price <= self.stop_loss_close_position_price + } + } +} + #[derive( Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug, Pod, Zeroable, )] @@ -364,6 +437,46 @@ pub struct Custody { pub borrow_rate_state: BorrowRateState, } +impl Custody { + // Returns the interest amount that has accrued since the last position cumulative interest snapshot update + pub fn get_interest_amount_usd( + &self, + position: &Position, + current_time: i64, + ) -> anyhow::Result { + if position.borrow_size_usd == 0 { + return Ok(0); + } + + let cumulative_interest = self.get_cumulative_interest(current_time)?; + + let position_interest = + if cumulative_interest > position.cumulative_interest_snapshot.to_u128() { + cumulative_interest - position.cumulative_interest_snapshot.to_u128() + } else { + return Ok(0); + }; + + math::checked_as_u64( + (position_interest * position.borrow_size_usd as u128) / Cortex::RATE_POWER, + ) + } + + pub fn get_cumulative_interest(&self, current_time: i64) -> anyhow::Result { + if current_time > self.borrow_rate_state.last_update { + let cumulative_interest = math::checked_ceil_div( + (current_time - self.borrow_rate_state.last_update) as u128 + * self.borrow_rate_state.current_rate as u128, + 3_600, + )?; + + Ok(self.borrow_rate_state.cumulative_interest.to_u128() + cumulative_interest) + } else { + Ok(self.borrow_rate_state.cumulative_interest.to_u128()) + } + } +} + #[derive(Copy, Clone, Eq, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] pub struct OraclePrice { pub price: u64,