precolator/
liquidation.rs1use serde::{Deserialize, Serialize};
4use solana_program::pubkey::Pubkey;
5use crate::errors::{ProgramError, Result};
6use crate::state::{Position, PositionStatus};
7use crate::constants::*;
8
9#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
10pub enum LiquidationTrigger {
11 MaintenanceMarginBreached,
12 HealthFactorCritical,
13 OraclePriceReached,
14 BadDebt,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct LiquidationEvent {
19 pub position_id: Pubkey,
20 pub liquidator: Pubkey,
21 pub liquidation_price: u64,
22 pub realized_pnl: i64,
23 pub liquidation_fee: u64,
24 pub insurance_payout: u64,
25 pub trigger: LiquidationTrigger,
26 pub timestamp: i64,
27}
28
29pub struct LiquidationKeeper;
30
31impl LiquidationKeeper {
32 pub fn should_liquidate(
34 position: &Position,
35 current_price: u64,
36 health_factor: u16,
37 ) -> Result<Option<LiquidationTrigger>> {
38 if position.status != PositionStatus::Open {
40 return Ok(None);
41 }
42
43 if health_factor < MAINTENANCE_MARGIN_BPS {
45 return Ok(Some(LiquidationTrigger::MaintenanceMarginBreached));
46 }
47
48 if current_price <= position.liquidation_price {
50 return Ok(Some(LiquidationTrigger::OraclePriceReached));
51 }
52
53 if health_factor < 500 { return Ok(Some(LiquidationTrigger::HealthFactorCritical));
56 }
57
58 Ok(None)
59 }
60
61 pub fn calculate_liquidation_fee(
63 position_value: u64,
64 ) -> Result<u64> {
65 let fee = (position_value as u128 * LIQUIDATION_FEE_BPS as u128) / 10_000;
66 Ok(fee as u64)
67 }
68
69 pub fn calculate_insurance_payout(
71 pnl: i64,
72 available_insurance: u64,
73 ) -> Result<u64> {
74 if pnl >= 0 {
75 return Ok(0); }
77
78 let abs_loss = (-pnl) as u64;
79 let payout = abs_loss.min(available_insurance);
80
81 Ok(payout)
82 }
83
84 pub fn execute_liquidation(
86 position: &mut Position,
87 liquidator: Pubkey,
88 liquidation_price: u64,
89 insurance_fund: &mut u64,
90 ) -> Result<LiquidationEvent> {
91 if position.status != PositionStatus::Open {
93 return Err(ProgramError::CannotLiquidate);
94 }
95
96 let pnl = crate::position::PositionManager::calculate_pnl(position, liquidation_price)?;
98
99 let position_value = crate::position::PositionManager::calculate_position_value(position)?;
101 let liquidation_fee = Self::calculate_liquidation_fee(position_value)?;
102
103 let insurance_payout = Self::calculate_insurance_payout(
105 pnl - (liquidation_fee as i64),
106 *insurance_fund,
107 )?;
108
109 *insurance_fund = insurance_fund.saturating_sub(insurance_payout);
111
112 position.status = PositionStatus::Liquidated;
114 position.pnl = pnl;
115
116 Ok(LiquidationEvent {
117 position_id: position.position_id,
118 liquidator,
119 liquidation_price,
120 realized_pnl: pnl,
121 liquidation_fee,
122 insurance_payout,
123 trigger: LiquidationTrigger::MaintenanceMarginBreached,
124 timestamp: 0,
125 })
126 }
127
128 pub fn check_liquidation_cooldown(
130 last_liquidation_slot: u64,
131 current_slot: u64,
132 ) -> bool {
133 let slots_passed = current_slot.saturating_sub(last_liquidation_slot);
134 slots_passed >= LIQUIDATION_COOLDOWN_SLOTS
135 }
136
137 pub fn estimate_insurance_impact(
139 total_open_interest: u64,
140 default_rate_bps: u16,
141 insurance_fund: u64,
142 ) -> Result<(u64, bool)> {
143 let estimated_losses = (total_open_interest as u128 * default_rate_bps as u128) / 10_000;
144 let remaining_insurance = insurance_fund.saturating_sub(estimated_losses as u64);
145
146 let is_solvent = remaining_insurance > 0;
147
148 Ok((remaining_insurance, is_solvent))
149 }
150
151 pub fn batch_check_liquidations(
153 positions: &[Position],
154 current_price: u64,
155 ) -> Result<Vec<Pubkey>> {
156 let mut liquidatable = Vec::new();
157
158 for position in positions {
159 if position.status == PositionStatus::Open {
160 if let Ok(health) = crate::position::PositionManager::calculate_health_factor(
161 position,
162 current_price,
163 ) {
164 if health < MAINTENANCE_MARGIN_BPS {
165 liquidatable.push(position.position_id);
166 }
167 }
168 }
169 }
170
171 Ok(liquidatable)
172 }
173
174 pub fn calculate_liquidator_reward(
176 liquidation_fee: u64,
177 _position_size: u64,
178 ) -> Result<u64> {
179 let reward = liquidation_fee / 2;
181 Ok(reward)
182 }
183
184 pub fn validate_liquidation(
186 position: &Position,
187 liquidation_price: u64,
188 ) -> Result<()> {
189 if position.status != PositionStatus::Open {
190 return Err(ProgramError::CannotLiquidate);
191 }
192
193 if liquidation_price == 0 {
194 return Err(ProgramError::InvalidAmount);
195 }
196
197 let price_change_percent = if liquidation_price > position.entry_price {
199 ((liquidation_price - position.entry_price) as u128 * 100) / position.entry_price as u128
200 } else {
201 ((position.entry_price - liquidation_price) as u128 * 100) / position.entry_price as u128
202 } as u32;
203
204 if price_change_percent > 200 {
205 return Err(ProgramError::InvalidConfiguration);
207 }
208
209 Ok(())
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::state::PositionSide;
217
218 #[test]
219 fn test_calculate_liquidation_fee() {
220 let result = LiquidationKeeper::calculate_liquidation_fee(1_000_000);
221 assert!(result.is_ok());
222 let fee = result.unwrap();
223 assert_eq!(fee, 50_000); }
225
226 #[test]
227 fn test_calculate_insurance_payout() {
228 let result = LiquidationKeeper::calculate_insurance_payout(-500_000, 1_000_000);
229 assert!(result.is_ok());
230 assert_eq!(result.unwrap(), 500_000);
231 }
232}