mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
503 lines
14 KiB
Go
503 lines
14 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dex
|
|
|
|
import (
|
|
"math/big"
|
|
"sync"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// Precompile address
|
|
var liquidatorAddr = common.HexToAddress(LXLiquidatorAddress)
|
|
|
|
// Storage key prefixes for Liquidator state
|
|
var (
|
|
liquidatorConfigPrefix = []byte("liq/cfg")
|
|
liquidatorEventPrefix = []byte("liq/evt")
|
|
)
|
|
|
|
// Liquidator handles liquidation of undercollateralized positions
|
|
// Features:
|
|
// - Partial and full liquidation
|
|
// - Configurable liquidation bonus
|
|
// - Health factor based trigger
|
|
// - Close factor limits (max % per liquidation)
|
|
type Liquidator struct {
|
|
mu sync.RWMutex
|
|
|
|
// Reference to lending pool
|
|
lendingPool *LendingPool
|
|
|
|
// Global configuration
|
|
config *LiquidatorConfig
|
|
|
|
// Liquidation history (optional, for events)
|
|
liquidations []*LiquidationEvent
|
|
}
|
|
|
|
// LiquidatorConfig holds global liquidation parameters
|
|
type LiquidatorConfig struct {
|
|
// Close factor - maximum % of debt that can be liquidated at once
|
|
// Scaled by 1e18 (e.g., 0.5e18 = 50%)
|
|
CloseFactor *big.Int
|
|
|
|
// Minimum health factor threshold for liquidation
|
|
// Positions with health factor < this can be liquidated
|
|
// Scaled by 1e18 (e.g., 1e18 = 1.0)
|
|
LiquidationThreshold *big.Int
|
|
|
|
// Protocol fee on liquidation bonus
|
|
// Scaled by 1e18 (e.g., 0.1e18 = 10% of bonus)
|
|
ProtocolFee *big.Int
|
|
|
|
// Minimum liquidation amount (in underlying)
|
|
MinLiquidation *big.Int
|
|
|
|
// Whether flash liquidations are enabled
|
|
FlashLiquidationEnabled bool
|
|
}
|
|
|
|
// LiquidationEvent records a liquidation
|
|
type LiquidationEvent struct {
|
|
Liquidator common.Address
|
|
Borrower common.Address
|
|
Asset common.Address
|
|
DebtRepaid *big.Int
|
|
CollateralSeized *big.Int
|
|
Bonus *big.Int
|
|
Timestamp uint64
|
|
}
|
|
|
|
// NewLiquidator creates a new Liquidator instance
|
|
func NewLiquidator(lendingPool *LendingPool) *Liquidator {
|
|
return &Liquidator{
|
|
lendingPool: lendingPool,
|
|
config: DefaultLiquidatorConfig(),
|
|
liquidations: make([]*LiquidationEvent, 0),
|
|
}
|
|
}
|
|
|
|
// DefaultLiquidatorConfig returns default configuration
|
|
func DefaultLiquidatorConfig() *LiquidatorConfig {
|
|
return &LiquidatorConfig{
|
|
CloseFactor: new(big.Int).Div(new(big.Int).Mul(big.NewInt(50), RAY), big.NewInt(100)), // 50%
|
|
LiquidationThreshold: new(big.Int).Set(RAY), // 1.0
|
|
ProtocolFee: new(big.Int).Div(new(big.Int).Mul(big.NewInt(10), RAY), big.NewInt(100)), // 10%
|
|
MinLiquidation: big.NewInt(0),
|
|
FlashLiquidationEnabled: true,
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Admin Functions
|
|
// =========================================================================
|
|
|
|
// SetCloseFactor updates the close factor
|
|
func (l *Liquidator) SetCloseFactor(closeFactor *big.Int) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if closeFactor.Sign() <= 0 || closeFactor.Cmp(RAY) > 0 {
|
|
return ErrInvalidParameter
|
|
}
|
|
|
|
l.config.CloseFactor = new(big.Int).Set(closeFactor)
|
|
return nil
|
|
}
|
|
|
|
// SetProtocolFee updates the protocol fee on liquidations
|
|
func (l *Liquidator) SetProtocolFee(fee *big.Int) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if fee.Sign() < 0 || fee.Cmp(RAY) > 0 {
|
|
return ErrInvalidParameter
|
|
}
|
|
|
|
l.config.ProtocolFee = new(big.Int).Set(fee)
|
|
return nil
|
|
}
|
|
|
|
// =========================================================================
|
|
// Core Liquidation Functions
|
|
// =========================================================================
|
|
|
|
// Liquidate allows a liquidator to repay debt and seize collateral
|
|
// with a bonus from an undercollateralized position
|
|
func (l *Liquidator) Liquidate(
|
|
stateDB StateDB,
|
|
liquidator common.Address,
|
|
borrower common.Address,
|
|
asset common.Address,
|
|
debtToRepay *big.Int,
|
|
) (*big.Int, *big.Int, error) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
// Get lending pool (thread-safe, but we already have the lock)
|
|
reserve := l.lendingPool.GetReserve(asset)
|
|
if reserve == nil {
|
|
return nil, nil, ErrReserveNotFound
|
|
}
|
|
|
|
// Get borrower's position
|
|
position := l.lendingPool.GetPosition(stateDB, borrower, asset)
|
|
if position == nil || position.BorrowAmount.Sign() == 0 {
|
|
return nil, nil, ErrNoDebtToRepay
|
|
}
|
|
|
|
// Check health factor
|
|
healthFactor := l.lendingPool.GetHealthFactor(stateDB, borrower, asset)
|
|
if healthFactor.Cmp(l.config.LiquidationThreshold) >= 0 {
|
|
return nil, nil, ErrPositionHealthy
|
|
}
|
|
|
|
// Calculate maximum liquidatable amount
|
|
maxLiquidatable := l.calculateMaxLiquidatable(position, reserve)
|
|
|
|
// Cap debt repayment to max liquidatable
|
|
actualDebtToRepay := debtToRepay
|
|
if actualDebtToRepay.Cmp(maxLiquidatable) > 0 {
|
|
actualDebtToRepay = new(big.Int).Set(maxLiquidatable)
|
|
}
|
|
|
|
// Check minimum liquidation
|
|
if actualDebtToRepay.Cmp(l.config.MinLiquidation) < 0 {
|
|
return nil, nil, ErrLiquidationTooSmall
|
|
}
|
|
|
|
// Calculate collateral to seize (debt + bonus)
|
|
collateralToSeize, bonus := l.calculateCollateralToSeize(actualDebtToRepay, reserve)
|
|
|
|
// Calculate supply value of borrower
|
|
supplyValue := new(big.Int).Mul(position.SupplyShares, reserve.ExchangeRate)
|
|
supplyValue.Div(supplyValue, RAY)
|
|
|
|
// Cap collateral seizure to available
|
|
if collateralToSeize.Cmp(supplyValue) > 0 {
|
|
collateralToSeize = new(big.Int).Set(supplyValue)
|
|
// Recalculate debt from collateral (without bonus)
|
|
actualDebtToRepay = new(big.Int).Mul(collateralToSeize, RAY)
|
|
actualDebtToRepay.Div(actualDebtToRepay, new(big.Int).Add(RAY, reserve.LiquidationBonus))
|
|
bonus = new(big.Int).Sub(collateralToSeize, actualDebtToRepay)
|
|
}
|
|
|
|
// Execute liquidation
|
|
err := l.executeLiquidation(
|
|
stateDB,
|
|
liquidator,
|
|
borrower,
|
|
asset,
|
|
actualDebtToRepay,
|
|
collateralToSeize,
|
|
reserve,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Record event
|
|
l.liquidations = append(l.liquidations, &LiquidationEvent{
|
|
Liquidator: liquidator,
|
|
Borrower: borrower,
|
|
Asset: asset,
|
|
DebtRepaid: actualDebtToRepay,
|
|
CollateralSeized: collateralToSeize,
|
|
Bonus: bonus,
|
|
Timestamp: stateDB.GetBlockNumber(),
|
|
})
|
|
|
|
return actualDebtToRepay, collateralToSeize, nil
|
|
}
|
|
|
|
// LiquidateWithFlash allows liquidation using flash loan for capital efficiency
|
|
func (l *Liquidator) LiquidateWithFlash(
|
|
stateDB StateDB,
|
|
liquidator common.Address,
|
|
borrower common.Address,
|
|
asset common.Address,
|
|
debtToRepay *big.Int,
|
|
) (*big.Int, *big.Int, error) {
|
|
if !l.config.FlashLiquidationEnabled {
|
|
return nil, nil, ErrFlashLiquidationDisabled
|
|
}
|
|
|
|
// Flash loan the debt amount
|
|
// In a real implementation, this would:
|
|
// 1. Borrow from pool manager flash loan
|
|
// 2. Repay borrower's debt
|
|
// 3. Receive collateral
|
|
// 4. Swap collateral to repay flash loan
|
|
// 5. Keep profit
|
|
|
|
// For this implementation, proceed with regular liquidation
|
|
// The liquidator needs to have the funds
|
|
return l.Liquidate(stateDB, liquidator, borrower, asset, debtToRepay)
|
|
}
|
|
|
|
// =========================================================================
|
|
// View Functions
|
|
// =========================================================================
|
|
|
|
// GetLiquidatableAmount returns how much debt can be liquidated
|
|
func (l *Liquidator) GetLiquidatableAmount(
|
|
stateDB StateDB,
|
|
borrower common.Address,
|
|
asset common.Address,
|
|
) *big.Int {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
reserve := l.lendingPool.GetReserve(asset)
|
|
if reserve == nil {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
position := l.lendingPool.GetPosition(stateDB, borrower, asset)
|
|
if position == nil || position.BorrowAmount.Sign() == 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
healthFactor := l.lendingPool.GetHealthFactor(stateDB, borrower, asset)
|
|
if healthFactor.Cmp(l.config.LiquidationThreshold) >= 0 {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
return l.calculateMaxLiquidatable(position, reserve)
|
|
}
|
|
|
|
// GetLiquidationBonus returns the bonus for liquidating a position
|
|
func (l *Liquidator) GetLiquidationBonus(
|
|
stateDB StateDB,
|
|
borrower common.Address,
|
|
asset common.Address,
|
|
debtToRepay *big.Int,
|
|
) *big.Int {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
reserve := l.lendingPool.GetReserve(asset)
|
|
if reserve == nil {
|
|
return big.NewInt(0)
|
|
}
|
|
|
|
_, bonus := l.calculateCollateralToSeize(debtToRepay, reserve)
|
|
return bonus
|
|
}
|
|
|
|
// IsLiquidatable checks if a position can be liquidated
|
|
func (l *Liquidator) IsLiquidatable(
|
|
stateDB StateDB,
|
|
borrower common.Address,
|
|
asset common.Address,
|
|
) bool {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
healthFactor := l.lendingPool.GetHealthFactor(stateDB, borrower, asset)
|
|
return healthFactor.Cmp(l.config.LiquidationThreshold) < 0
|
|
}
|
|
|
|
// GetLiquidationHistory returns recent liquidations
|
|
func (l *Liquidator) GetLiquidationHistory(limit int) []*LiquidationEvent {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
if limit <= 0 || limit > len(l.liquidations) {
|
|
limit = len(l.liquidations)
|
|
}
|
|
|
|
// Return most recent
|
|
start := max(len(l.liquidations)-limit, 0)
|
|
|
|
result := make([]*LiquidationEvent, limit)
|
|
copy(result, l.liquidations[start:])
|
|
return result
|
|
}
|
|
|
|
// GetConfig returns the liquidator configuration
|
|
func (l *Liquidator) GetConfig() *LiquidatorConfig {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return l.config
|
|
}
|
|
|
|
// =========================================================================
|
|
// Internal Functions
|
|
// =========================================================================
|
|
|
|
// calculateMaxLiquidatable calculates max debt that can be liquidated
|
|
func (l *Liquidator) calculateMaxLiquidatable(position *LendingPosition, reserve *Reserve) *big.Int {
|
|
// maxLiquidatable = debt * closeFactor
|
|
maxLiquidatable := new(big.Int).Mul(position.BorrowAmount, l.config.CloseFactor)
|
|
maxLiquidatable.Div(maxLiquidatable, RAY)
|
|
return maxLiquidatable
|
|
}
|
|
|
|
// calculateCollateralToSeize calculates collateral to seize including bonus
|
|
func (l *Liquidator) calculateCollateralToSeize(debtToRepay *big.Int, reserve *Reserve) (*big.Int, *big.Int) {
|
|
// collateral = debt + (debt * bonus)
|
|
// bonus is scaled by RAY
|
|
bonus := new(big.Int).Mul(debtToRepay, reserve.LiquidationBonus)
|
|
bonus.Div(bonus, RAY)
|
|
|
|
collateral := new(big.Int).Add(debtToRepay, bonus)
|
|
return collateral, bonus
|
|
}
|
|
|
|
// executeLiquidation performs the actual liquidation
|
|
func (l *Liquidator) executeLiquidation(
|
|
stateDB StateDB,
|
|
liquidator common.Address,
|
|
borrower common.Address,
|
|
asset common.Address,
|
|
debtToRepay *big.Int,
|
|
collateralToSeize *big.Int,
|
|
reserve *Reserve,
|
|
) error {
|
|
// Transfer debt payment from liquidator to pool
|
|
amountU256, _ := uint256.FromBig(debtToRepay)
|
|
stateDB.SubBalance(liquidator, amountU256)
|
|
stateDB.AddBalance(lendingPoolAddr, amountU256)
|
|
|
|
// Get borrower position
|
|
borrowerKey := positionKey(borrower, asset)
|
|
borrowerPosition := l.lendingPool.positions[borrowerKey]
|
|
if borrowerPosition == nil {
|
|
return ErrPositionNotFound
|
|
}
|
|
|
|
// Calculate shares to seize
|
|
// shares = collateral * RAY / exchangeRate
|
|
sharesToSeize := new(big.Int).Mul(collateralToSeize, RAY)
|
|
sharesToSeize.Div(sharesToSeize, reserve.ExchangeRate)
|
|
|
|
// Cap to available shares
|
|
if sharesToSeize.Cmp(borrowerPosition.SupplyShares) > 0 {
|
|
sharesToSeize = new(big.Int).Set(borrowerPosition.SupplyShares)
|
|
}
|
|
|
|
// Update borrower position
|
|
borrowerPosition.SupplyShares = new(big.Int).Sub(borrowerPosition.SupplyShares, sharesToSeize)
|
|
borrowerPosition.BorrowAmount = new(big.Int).Sub(borrowerPosition.BorrowAmount, debtToRepay)
|
|
if borrowerPosition.BorrowAmount.Sign() < 0 {
|
|
borrowerPosition.BorrowAmount = big.NewInt(0)
|
|
}
|
|
|
|
// Calculate protocol fee from bonus
|
|
bonus := new(big.Int).Sub(collateralToSeize, debtToRepay)
|
|
protocolFee := new(big.Int).Mul(bonus, l.config.ProtocolFee)
|
|
protocolFee.Div(protocolFee, RAY)
|
|
|
|
// Transfer collateral to liquidator (minus protocol fee)
|
|
liquidatorReceives := new(big.Int).Sub(collateralToSeize, protocolFee)
|
|
liquidatorReceivesU256, _ := uint256.FromBig(liquidatorReceives)
|
|
stateDB.SubBalance(lendingPoolAddr, liquidatorReceivesU256)
|
|
stateDB.AddBalance(liquidator, liquidatorReceivesU256)
|
|
|
|
// Protocol fee stays in pool as reserves
|
|
reserve.TotalReserves = new(big.Int).Add(reserve.TotalReserves, protocolFee)
|
|
|
|
// Update reserve totals
|
|
reserve.TotalSupply = new(big.Int).Sub(reserve.TotalSupply, collateralToSeize)
|
|
reserve.TotalBorrows = new(big.Int).Sub(reserve.TotalBorrows, debtToRepay)
|
|
|
|
// Save state
|
|
l.lendingPool.savePosition(stateDB, borrowerKey, borrowerPosition)
|
|
l.lendingPool.saveReserve(stateDB, reserve)
|
|
|
|
return nil
|
|
}
|
|
|
|
// =========================================================================
|
|
// Batch Liquidation
|
|
// =========================================================================
|
|
|
|
// BatchLiquidationTarget represents a position to liquidate
|
|
type BatchLiquidationTarget struct {
|
|
Borrower common.Address
|
|
Asset common.Address
|
|
DebtToRepay *big.Int
|
|
}
|
|
|
|
// BatchLiquidate liquidates multiple positions in one call
|
|
func (l *Liquidator) BatchLiquidate(
|
|
stateDB StateDB,
|
|
liquidator common.Address,
|
|
targets []BatchLiquidationTarget,
|
|
) (totalDebtRepaid, totalCollateralSeized *big.Int, errors []error) {
|
|
totalDebtRepaid = big.NewInt(0)
|
|
totalCollateralSeized = big.NewInt(0)
|
|
errors = make([]error, len(targets))
|
|
|
|
for i, target := range targets {
|
|
debtRepaid, collateralSeized, err := l.Liquidate(
|
|
stateDB,
|
|
liquidator,
|
|
target.Borrower,
|
|
target.Asset,
|
|
target.DebtToRepay,
|
|
)
|
|
|
|
if err != nil {
|
|
errors[i] = err
|
|
continue
|
|
}
|
|
|
|
totalDebtRepaid.Add(totalDebtRepaid, debtRepaid)
|
|
totalCollateralSeized.Add(totalCollateralSeized, collateralSeized)
|
|
}
|
|
|
|
return totalDebtRepaid, totalCollateralSeized, errors
|
|
}
|
|
|
|
// FindLiquidatablePositions searches for positions that can be liquidated
|
|
// This is a view function for off-chain liquidation bots
|
|
func (l *Liquidator) FindLiquidatablePositions(
|
|
stateDB StateDB,
|
|
assets []common.Address,
|
|
) []BatchLiquidationTarget {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
targets := make([]BatchLiquidationTarget, 0)
|
|
|
|
for _, asset := range assets {
|
|
reserve := l.lendingPool.GetReserve(asset)
|
|
if reserve == nil {
|
|
continue
|
|
}
|
|
|
|
// Check all known positions for this asset
|
|
for key, position := range l.lendingPool.positions {
|
|
if position.BorrowAmount.Sign() == 0 {
|
|
continue
|
|
}
|
|
|
|
// Reconstruct user address from position
|
|
// In production, would need proper index
|
|
healthFactor := l.lendingPool.calculateHealthFactor(
|
|
new(big.Int).Mul(position.SupplyShares, reserve.ExchangeRate),
|
|
position.BorrowAmount,
|
|
reserve,
|
|
)
|
|
|
|
if healthFactor.Cmp(l.config.LiquidationThreshold) < 0 {
|
|
maxLiquidatable := l.calculateMaxLiquidatable(position, reserve)
|
|
targets = append(targets, BatchLiquidationTarget{
|
|
Borrower: position.Owner,
|
|
Asset: asset,
|
|
DebtToRepay: maxLiquidatable,
|
|
})
|
|
_ = key // silence unused warning
|
|
}
|
|
}
|
|
}
|
|
|
|
return targets
|
|
}
|