use serde::{Deserialize, Serialize};
use solana_program::pubkey::Pubkey;
use crate::errors::{ProgramError, Result};
use crate::state::{Position, PositionStatus};
use crate::constants::*;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum LiquidationTrigger {
MaintenanceMarginBreached,
HealthFactorCritical,
OraclePriceReached,
BadDebt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidationEvent {
pub position_id: Pubkey,
pub liquidator: Pubkey,
pub liquidation_price: u64,
pub realized_pnl: i64,
pub liquidation_fee: u64,
pub insurance_payout: u64,
pub trigger: LiquidationTrigger,
pub timestamp: i64,
}
pub struct LiquidationKeeper;
impl LiquidationKeeper {
pub fn should_liquidate(
position: &Position,
current_price: u64,
health_factor: u16,
) -> Result<Option<LiquidationTrigger>> {
if position.status != PositionStatus::Open {
return Ok(None);
}
if health_factor < MAINTENANCE_MARGIN_BPS {
return Ok(Some(LiquidationTrigger::MaintenanceMarginBreached));
}
if current_price <= position.liquidation_price {
return Ok(Some(LiquidationTrigger::OraclePriceReached));
}
if health_factor < 500 { return Ok(Some(LiquidationTrigger::HealthFactorCritical));
}
Ok(None)
}
pub fn calculate_liquidation_fee(
position_value: u64,
) -> Result<u64> {
let fee = (position_value as u128 * LIQUIDATION_FEE_BPS as u128) / 10_000;
Ok(fee as u64)
}
pub fn calculate_insurance_payout(
pnl: i64,
available_insurance: u64,
) -> Result<u64> {
if pnl >= 0 {
return Ok(0); }
let abs_loss = (-pnl) as u64;
let payout = abs_loss.min(available_insurance);
Ok(payout)
}
pub fn execute_liquidation(
position: &mut Position,
liquidator: Pubkey,
liquidation_price: u64,
insurance_fund: &mut u64,
) -> Result<LiquidationEvent> {
if position.status != PositionStatus::Open {
return Err(ProgramError::CannotLiquidate);
}
let pnl = crate::position::PositionManager::calculate_pnl(position, liquidation_price)?;
let position_value = crate::position::PositionManager::calculate_position_value(position)?;
let liquidation_fee = Self::calculate_liquidation_fee(position_value)?;
let insurance_payout = Self::calculate_insurance_payout(
pnl - (liquidation_fee as i64),
*insurance_fund,
)?;
*insurance_fund = insurance_fund.saturating_sub(insurance_payout);
position.status = PositionStatus::Liquidated;
position.pnl = pnl;
Ok(LiquidationEvent {
position_id: position.position_id,
liquidator,
liquidation_price,
realized_pnl: pnl,
liquidation_fee,
insurance_payout,
trigger: LiquidationTrigger::MaintenanceMarginBreached,
timestamp: 0,
})
}
pub fn check_liquidation_cooldown(
last_liquidation_slot: u64,
current_slot: u64,
) -> bool {
let slots_passed = current_slot.saturating_sub(last_liquidation_slot);
slots_passed >= LIQUIDATION_COOLDOWN_SLOTS
}
pub fn estimate_insurance_impact(
total_open_interest: u64,
default_rate_bps: u16,
insurance_fund: u64,
) -> Result<(u64, bool)> {
let estimated_losses = (total_open_interest as u128 * default_rate_bps as u128) / 10_000;
let remaining_insurance = insurance_fund.saturating_sub(estimated_losses as u64);
let is_solvent = remaining_insurance > 0;
Ok((remaining_insurance, is_solvent))
}
pub fn batch_check_liquidations(
positions: &[Position],
current_price: u64,
) -> Result<Vec<Pubkey>> {
let mut liquidatable = Vec::new();
for position in positions {
if position.status == PositionStatus::Open {
if let Ok(health) = crate::position::PositionManager::calculate_health_factor(
position,
current_price,
) {
if health < MAINTENANCE_MARGIN_BPS {
liquidatable.push(position.position_id);
}
}
}
}
Ok(liquidatable)
}
pub fn calculate_liquidator_reward(
liquidation_fee: u64,
_position_size: u64,
) -> Result<u64> {
let reward = liquidation_fee / 2;
Ok(reward)
}
pub fn validate_liquidation(
position: &Position,
liquidation_price: u64,
) -> Result<()> {
if position.status != PositionStatus::Open {
return Err(ProgramError::CannotLiquidate);
}
if liquidation_price == 0 {
return Err(ProgramError::InvalidAmount);
}
let price_change_percent = if liquidation_price > position.entry_price {
((liquidation_price - position.entry_price) as u128 * 100) / position.entry_price as u128
} else {
((position.entry_price - liquidation_price) as u128 * 100) / position.entry_price as u128
} as u32;
if price_change_percent > 200 {
return Err(ProgramError::InvalidConfiguration);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::PositionSide;
#[test]
fn test_calculate_liquidation_fee() {
let result = LiquidationKeeper::calculate_liquidation_fee(1_000_000);
assert!(result.is_ok());
let fee = result.unwrap();
assert_eq!(fee, 50_000); }
#[test]
fn test_calculate_insurance_payout() {
let result = LiquidationKeeper::calculate_insurance_payout(-500_000, 1_000_000);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 500_000);
}
}