precolator-program 1.0.0

Core Rust library for the Precolator perpetual futures trading protocol on Solana — oracle management, position handling, risk engine, and liquidation system.
Documentation
// Liquidation module - Liquidation detection and execution

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 {
    /// Check if a position meets liquidation criteria
    pub fn should_liquidate(
        position: &Position,
        current_price: u64,
        health_factor: u16,
    ) -> Result<Option<LiquidationTrigger>> {
        // Check if position is still open
        if position.status != PositionStatus::Open {
            return Ok(None);
        }

        // Check maintenance margin
        if health_factor < MAINTENANCE_MARGIN_BPS {
            return Ok(Some(LiquidationTrigger::MaintenanceMarginBreached));
        }

        // Check if price reached liquidation level
        if current_price <= position.liquidation_price {
            return Ok(Some(LiquidationTrigger::OraclePriceReached));
        }

        // Check health factor critically low
        if health_factor < 500 { // 5% health
            return Ok(Some(LiquidationTrigger::HealthFactorCritical));
        }

        Ok(None)
    }

    /// Calculate liquidation fee for a position
    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)
    }

    /// Calculate insurance payout needed
    pub fn calculate_insurance_payout(
        pnl: i64,
        available_insurance: u64,
    ) -> Result<u64> {
        if pnl >= 0 {
            return Ok(0); // No insurance needed for profitable positions
        }

        let abs_loss = (-pnl) as u64;
        let payout = abs_loss.min(available_insurance);

        Ok(payout)
    }

    /// Execute liquidation
    pub fn execute_liquidation(
        position: &mut Position,
        liquidator: Pubkey,
        liquidation_price: u64,
        insurance_fund: &mut u64,
    ) -> Result<LiquidationEvent> {
        // Verify position can be liquidated
        if position.status != PositionStatus::Open {
            return Err(ProgramError::CannotLiquidate);
        }

        // Calculate realized P&L at liquidation price
        let pnl = crate::position::PositionManager::calculate_pnl(position, liquidation_price)?;
        
        // Calculate fees
        let position_value = crate::position::PositionManager::calculate_position_value(position)?;
        let liquidation_fee = Self::calculate_liquidation_fee(position_value)?;

        // Calculate insurance payout
        let insurance_payout = Self::calculate_insurance_payout(
            pnl - (liquidation_fee as i64),
            *insurance_fund,
        )?;

        // Update insurance fund
        *insurance_fund = insurance_fund.saturating_sub(insurance_payout);

        // Update position state
        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,
        })
    }

    /// Check if liquidation violates cooldown period
    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
    }

    /// Calculate liquidation impact on insurance fund
    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))
    }

    /// Batch liquidation check for multiple positions
    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)
    }

    /// Calculate liquidator incentive
    pub fn calculate_liquidator_reward(
        liquidation_fee: u64,
        _position_size: u64,
    ) -> Result<u64> {
        // Liquidator gets 50% of fee, rest goes to insurance
        let reward = liquidation_fee / 2;
        Ok(reward)
    }

    /// Validate liquidation parameters
    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);
        }

        // Verify liquidation price is within reasonable bounds
        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 {
            // More than 2x price change seems suspicious
            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); // 0.5% of 1M = 50K
    }

    #[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);
    }
}