Skip to main content

precolator/
liquidation.rs

1// Liquidation module - Liquidation detection and execution
2
3use 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    /// Check if a position meets liquidation criteria
33    pub fn should_liquidate(
34        position: &Position,
35        current_price: u64,
36        health_factor: u16,
37    ) -> Result<Option<LiquidationTrigger>> {
38        // Check if position is still open
39        if position.status != PositionStatus::Open {
40            return Ok(None);
41        }
42
43        // Check maintenance margin
44        if health_factor < MAINTENANCE_MARGIN_BPS {
45            return Ok(Some(LiquidationTrigger::MaintenanceMarginBreached));
46        }
47
48        // Check if price reached liquidation level
49        if current_price <= position.liquidation_price {
50            return Ok(Some(LiquidationTrigger::OraclePriceReached));
51        }
52
53        // Check health factor critically low
54        if health_factor < 500 { // 5% health
55            return Ok(Some(LiquidationTrigger::HealthFactorCritical));
56        }
57
58        Ok(None)
59    }
60
61    /// Calculate liquidation fee for a position
62    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    /// Calculate insurance payout needed
70    pub fn calculate_insurance_payout(
71        pnl: i64,
72        available_insurance: u64,
73    ) -> Result<u64> {
74        if pnl >= 0 {
75            return Ok(0); // No insurance needed for profitable positions
76        }
77
78        let abs_loss = (-pnl) as u64;
79        let payout = abs_loss.min(available_insurance);
80
81        Ok(payout)
82    }
83
84    /// Execute liquidation
85    pub fn execute_liquidation(
86        position: &mut Position,
87        liquidator: Pubkey,
88        liquidation_price: u64,
89        insurance_fund: &mut u64,
90    ) -> Result<LiquidationEvent> {
91        // Verify position can be liquidated
92        if position.status != PositionStatus::Open {
93            return Err(ProgramError::CannotLiquidate);
94        }
95
96        // Calculate realized P&L at liquidation price
97        let pnl = crate::position::PositionManager::calculate_pnl(position, liquidation_price)?;
98        
99        // Calculate fees
100        let position_value = crate::position::PositionManager::calculate_position_value(position)?;
101        let liquidation_fee = Self::calculate_liquidation_fee(position_value)?;
102
103        // Calculate insurance payout
104        let insurance_payout = Self::calculate_insurance_payout(
105            pnl - (liquidation_fee as i64),
106            *insurance_fund,
107        )?;
108
109        // Update insurance fund
110        *insurance_fund = insurance_fund.saturating_sub(insurance_payout);
111
112        // Update position state
113        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    /// Check if liquidation violates cooldown period
129    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    /// Calculate liquidation impact on insurance fund
138    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    /// Batch liquidation check for multiple positions
152    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    /// Calculate liquidator incentive
175    pub fn calculate_liquidator_reward(
176        liquidation_fee: u64,
177        _position_size: u64,
178    ) -> Result<u64> {
179        // Liquidator gets 50% of fee, rest goes to insurance
180        let reward = liquidation_fee / 2;
181        Ok(reward)
182    }
183
184    /// Validate liquidation parameters
185    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        // Verify liquidation price is within reasonable bounds
198        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            // More than 2x price change seems suspicious
206            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); // 0.5% of 1M = 50K
224    }
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}