AgentSkillsCN

Aave V4 Liquidation

当用户询问“清算”、“清算呼叫”、“目标健康系数”、“清算奖励”、“荷兰式拍卖”、“平仓因子”,或需要深入理解 Aave V4 重塑后的清算引擎时,应选用此技能。

SKILL.md
--- frontmatter
name: Aave V4 Liquidation
description: This skill should be used when the user asks about "liquidation", "liquidationCall", "target health factor", "liquidation bonus", "Dutch auction", "close factor", or needs to understand Aave V4's redesigned liquidation engine.
version: 0.1.0

Aave V4 Liquidation

Aave V4 introduces a redesigned liquidation mechanism with Target Health Factor, Dutch-auction style liquidation bonuses, and improved dust handling.

Overview

code
┌──────────────────────────────────────────────────────────────┐
│                 V4 LIQUIDATION REDESIGN                      │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  V3 Approach:                                                │
│  • Fixed 50% close factor                                    │
│  • Static liquidation bonus                                  │
│  • User often over-liquidated                                │
│                                                              │
│  V4 Approach:                                                │
│  • Target Health Factor (configurable)                       │
│  • Variable liquidation bonus (Dutch-auction)                │
│  • Liquidate just enough to reach target HF                  │
│  • Dynamic dust handling                                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Key Differences from V3

AspectV3V4
Close FactorFixed 50% (100% when HF < 0.95)Dynamic based on Target HF
Liquidation BonusStatic per assetVariable (Dutch-auction style)
GoalRepay fixed % of debtRestore to Target HF
User ExperienceOften over-liquidatedMinimally liquidated

Liquidation Configuration

solidity
struct LiquidationConfig {
    uint64 targetHealthFactor;      // Target HF after liquidation (>= 1e18)
    uint64 healthFactorForMaxBonus; // HF below which max bonus applies (< 1e18)
    uint16 liquidationBonusFactor;  // % of max bonus at threshold (BPS)
}

// Per-reserve configuration
struct DynamicReserveConfig {
    uint16 collateralFactor;        // Collateral Factor
    uint16 maxLiquidationBonus;     // Max bonus (e.g., 105_00 = 5% bonus)
    uint16 liquidationFee;          // Protocol fee on bonus (BPS)
}

Example Configuration

solidity
// Spoke-wide config
LiquidationConfig memory config = LiquidationConfig({
    targetHealthFactor: 1.05e18,       // Restore to 1.05 HF
    healthFactorForMaxBonus: 0.90e18,  // Max bonus when HF < 0.90
    liquidationBonusFactor: 80_00      // 80% of max bonus at threshold
});

// Per-reserve config (e.g., ETH)
DynamicReserveConfig memory ethConfig = DynamicReserveConfig({
    collateralFactor: 82_50,           // 82.5% CF
    maxLiquidationBonus: 105_00,       // 5% max bonus
    liquidationFee: 10_00              // 10% of bonus goes to protocol
});

Dutch-Auction Liquidation Bonus

The liquidation bonus varies based on the borrower's health factor:

code
┌──────────────────────────────────────────────────────────────┐
│           LIQUIDATION BONUS VS HEALTH FACTOR                 │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Bonus │                                                     │
│        │  maxLB ─────────────────────────────┐               │
│        │                                     │               │
│        │                              ╱      │               │
│        │                            ╱        │               │
│        │                          ╱          │               │
│        │  minLB ────────────────╱            │               │
│        └─────────────────────────────────────┼───────────    │
│                  hfForMaxBonus    1.0        │               │
│                    (0.90)      (threshold)                   │
│                                                              │
│  minLB = (maxLB - 100%) × lbFactor + 100%                    │
│  Example: maxLB=105%, lbFactor=80%                           │
│  minLB = 5% × 80% + 100% = 104%                              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Bonus Calculation

solidity
library LiquidationLogic {
    uint64 internal constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18;

    /// @notice Calculate liquidation bonus based on health factor
    function calculateLiquidationBonus(
        uint256 healthFactor,
        uint256 maxLiquidationBonus,
        uint256 healthFactorForMaxBonus,
        uint256 liquidationBonusFactor
    ) internal pure returns (uint256) {
        if (healthFactor <= healthFactorForMaxBonus) {
            // Max bonus region
            return maxLiquidationBonus;
        }

        // Linear interpolation between min and max
        uint256 minBonus = PercentageMath.PERCENTAGE_FACTOR +
            (maxLiquidationBonus - PercentageMath.PERCENTAGE_FACTOR)
                .percentMul(liquidationBonusFactor);

        uint256 range = HEALTH_FACTOR_LIQUIDATION_THRESHOLD - healthFactorForMaxBonus;
        uint256 position = healthFactor - healthFactorForMaxBonus;

        return maxLiquidationBonus -
            (maxLiquidationBonus - minBonus).mulDiv(position, range);
    }
}

Target Health Factor

V4 liquidates just enough to restore the user to Target HF:

solidity
/// @notice Calculate debt to liquidate to reach target HF
function calculateDebtToLiquidate(
    uint256 totalCollateralValue,
    uint256 avgCollateralFactor,
    uint256 totalDebtValue,
    uint256 targetHealthFactor,
    uint256 liquidationBonus
) internal pure returns (uint256) {
    // Current HF = (collateral × CF) / debt
    // Target HF after liquidation:
    // (collateral - seized) × CF / (debt - repaid) = targetHF

    // Solve for debt to repay:
    uint256 numerator = totalDebtValue.wadMul(targetHealthFactor) -
        totalCollateralValue.percentMul(avgCollateralFactor);

    uint256 denominator = targetHealthFactor -
        liquidationBonus.percentMul(avgCollateralFactor);

    return numerator.wadDiv(denominator);
}

Example

code
User Position:
- Collateral: $10,000 (CF = 80%)
- Debt: $8,500
- HF = $10,000 × 0.80 / $8,500 = 0.94 (liquidatable!)

Target HF: 1.05
Liquidation Bonus: 104% (at HF 0.94)

Debt to liquidate: $2,127
Collateral seized: $2,127 × 1.04 = $2,212

After liquidation:
- Collateral: $10,000 - $2,212 = $7,788
- Debt: $8,500 - $2,127 = $6,373
- New HF = $7,788 × 0.80 / $6,373 = 0.977... ≈ 1.05 ✓

Liquidation Call

solidity
/// @notice Liquidate a user position
function liquidationCall(
    uint256 collateralReserveId,
    uint256 debtReserveId,
    address user,
    uint256 debtToCover,
    bool receiveShares
) external {
    // 1. Validate liquidation eligibility
    require(user != msg.sender, CannotLiquidateSelf());
    uint256 healthFactor = getUserHealthFactor(user);
    require(healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD, HealthyPosition());

    // 2. Get reserves and positions
    Reserve storage collateralReserve = _reserves[collateralReserveId];
    Reserve storage debtReserve = _reserves[debtReserveId];
    require(collateralReserve.flags.liquidatable(), NotLiquidatable());

    // 3. Calculate liquidation bonus
    DynamicReserveConfig storage config = _dynamicConfig[collateralReserveId][...];
    uint256 liquidationBonus = LiquidationLogic.calculateLiquidationBonus(
        healthFactor,
        config.maxLiquidationBonus,
        _liquidationConfig.healthFactorForMaxBonus,
        _liquidationConfig.liquidationBonusFactor
    );

    // 4. Calculate amounts
    (
        uint256 actualDebtToLiquidate,
        uint256 collateralToSeize,
        uint256 protocolFee
    ) = LiquidationLogic.calculateLiquidationAmounts(
        user,
        debtToCover,
        collateralReserveId,
        debtReserveId,
        liquidationBonus,
        config.liquidationFee,
        _liquidationConfig.targetHealthFactor
    );

    // 5. Handle debt repayment
    _handleDebtRepayment(debtReserveId, user, actualDebtToLiquidate);

    // 6. Handle collateral transfer
    _handleCollateralSeizure(
        collateralReserveId,
        user,
        msg.sender,
        collateralToSeize,
        protocolFee,
        receiveShares
    );

    // 7. Handle deficit if needed
    _handleDeficitIfNeeded(user);

    emit LiquidationCall(...);
}

Dust Handling

V4 prevents tiny leftover positions:

solidity
uint256 internal constant DUST_LIQUIDATION_THRESHOLD = 1_000e26; // $1,000 in base units

/// @notice Adjust liquidation to prevent dust
function adjustForDust(
    uint256 remainingDebt,
    uint256 maxLiquidatable,
    bool fullRepayIntent
) internal pure returns (uint256) {
    // If remaining debt would be below threshold
    if (remainingDebt - maxLiquidatable < DUST_LIQUIDATION_THRESHOLD) {
        if (fullRepayIntent) {
            // Allow full liquidation
            return remainingDebt;
        } else {
            // Revert to prevent dust
            revert DustDebtRemaining();
        }
    }
    return maxLiquidatable;
}

Dust Scenarios

code
┌──────────────────────────────────────────────────────────────┐
│                    DUST HANDLING                             │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Scenario 1: Normal liquidation                              │
│  Remaining debt: $5,000                                      │
│  To liquidate: $2,000                                        │
│  After: $3,000 (> $1,000 threshold) ✓                        │
│                                                              │
│  Scenario 2: Would create dust                               │
│  Remaining debt: $1,500                                      │
│  To liquidate: $800                                          │
│  After: $700 (< $1,000 threshold)                            │
│  → If fullRepayIntent: liquidate full $1,500                 │
│  → Otherwise: revert                                         │
│                                                              │
│  Scenario 3: Collateral exhausted                            │
│  All collateral seized, debt remains                         │
│  → Report deficit to Hub                                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Protocol Fee

A portion of the liquidation bonus goes to the protocol:

solidity
// Liquidation fee is a % of the bonus (not the total)
uint256 bonusAmount = collateralValue.percentMul(liquidationBonus) - collateralValue;
uint256 protocolFee = bonusAmount.percentMul(config.liquidationFee);

// Liquidator receives: collateral × bonus - protocolFee
// Protocol receives: protocolFee (as shares via Hub)

Example

code
Debt repaid: $1,000
Liquidation bonus: 5%
Gross collateral seized: $1,050
Bonus amount: $50
Protocol fee: $50 × 10% = $5

Liquidator receives: $1,045 worth of collateral
Protocol receives: $5 worth of shares

Deficit Handling

If a user has no collateral left but still has debt:

solidity
function _handleDeficitIfNeeded(address user) internal {
    if (_hasRemainingDebt(user) && !_hasRemainingCollateral(user)) {
        // Report deficit to Hub
        for each debt reserve {
            (uint256 drawnDebt, uint256 premiumDebt) = getUserDebt(reserveId, user);
            uint256 totalDeficit = drawnDebt + premiumDebt;

            IHubBase.PremiumDelta memory premiumDelta = ...;

            reserve.hub.reportDeficit(
                reserve.assetId,
                drawnDebt,
                premiumDelta
            );

            // Clear user position
            delete _userPositions[user][reserveId];
        }
    }
}

View Functions

solidity
/// @notice Get liquidation bonus for current health factor
function getLiquidationBonus(
    uint256 collateralReserveId,
    address user
) external view returns (uint256) {
    uint256 hf = getUserHealthFactor(user);
    DynamicReserveConfig storage config = _getDynamicConfig(collateralReserveId, user);

    return LiquidationLogic.calculateLiquidationBonus(
        hf,
        config.maxLiquidationBonus,
        _liquidationConfig.healthFactorForMaxBonus,
        _liquidationConfig.liquidationBonusFactor
    );
}

/// @notice Preview liquidation amounts
function previewLiquidation(
    uint256 collateralReserveId,
    uint256 debtReserveId,
    address user,
    uint256 debtToCover
) external view returns (
    uint256 actualDebtToLiquidate,
    uint256 collateralToSeize,
    uint256 protocolFee
);

Events

solidity
event LiquidationCall(
    uint256 indexed collateralReserveId,
    uint256 indexed debtReserveId,
    address indexed user,
    address liquidator,
    bool receiveShares,
    uint256 debtToLiquidate,
    uint256 drawnSharesToLiquidate,
    IHubBase.PremiumDelta premiumDelta,
    uint256 collateralToLiquidate,
    uint256 collateralSharesToLiquidate,
    uint256 collateralSharesToLiquidator
);

event UpdateLiquidationConfig(LiquidationConfig config);

Reference Files

  • src/spoke/Spoke.sol - liquidationCall implementation
  • src/spoke/libraries/LiquidationLogic.sol - Liquidation calculations
  • src/spoke/interfaces/ISpoke.sol - Liquidation types
  • docs/overview.md - Liquidation design documentation