Skip to content
3F Docs

Pre-Liquidation

Pre-liquidation is the mechanism that allows anyone to liquidate a Grunt borrow position that has become unhealthy at its liquidation LTV, even though it remains healthy at Morpho Blue’s native market LLTV. This page covers the math, the function interface, and the callback pattern for flash-liquidation integrations.

A position’s health is determined by comparing its maximum borrowing capacity to its outstanding debt:

collateralValue = collateral * oraclePrice / ORACLE_PRICE_SCALE
maxBorrow = collateralValue * lltv
healthy = maxBorrow >= totalBorrowed

When isHealthy(liquidationLtv()) returns false but isHealthy(marketLltv) would still return true, the position is in the pre-liquidation zone. This is the window where the proportional path of preLiquidate applies.

preLiquidate also serves an earlier band: when the position’s LTV is between safeLtv() and liquidationLtv(), the same call consumes standing offers instead of liquidating proportionally. See Offer Pre-Liquidation. This page describes the proportional path only.

Pre-liquidation uses proportional liquidation. When you repay a fraction of the position’s debt, you receive the same fraction of the position’s collateral. The bonus comes from the fact that the collateral is worth more than the debt you repaid.

Formula:

Liquidation Bonus = 1 - LTV (at liquidation time)

Worked example:

Consider a position at 80% LTV:

  • Collateral: 100 tokens worth $100
  • Debt: $80
  • LTV: 80%

A liquidator repays 50% of the debt ($40) and seizes 50% of the collateral (50 tokens worth $50).

Before After
Collateral 100 ($100) 50 ($50)
Debt $80 $40
LTV 80% 80%
Liquidator spent — $40
Liquidator received — $50
Liquidator profit — $10 (25% on capital)

The effective bonus rate on the collateral is 1 - 0.80 = 20%. The return on capital deployed is $10 / $40 = 25%.

Key properties:

  • Liquidators receive a proportional share of collateral relative to the debt they repay
  • The bonus scales with how underwater the position is — a position at 90% LTV yields a 10% bonus, while one at 70% LTV yields a 30% bonus
  • There is no cap on seized collateral beyond the position’s total collateral
  • The position’s LTV remains unchanged after a proportional liquidation

Pure proportionality breaks once the position’s LTV exceeds the Morpho market’s own LLTV. Removing the same fraction of debt and collateral preserves the LTV, so the post-withdrawal state would fail Morpho’s internal health check and the whole call would revert.

The position adjusts for this, in whichever direction the liquidator pinned:

Mode Adjustment
seizedAssets The contract computes the minimum debt repayment that leaves the position healthy on Morpho after the withdrawal. At least proportional, potentially more
repaidShares The contract computes the maximum collateral seizable while leaving the position healthy on Morpho after the withdrawal. At most proportional, potentially less

So for a small partial fill on a deeply underwater position, more debt can be repaid than the seized collateral is worth. The priority in that state is restoring health on the underlying market, not the liquidator’s margin. Above the market LLTV, check the returned amounts rather than assuming the 1 - LTV bonus.

function preLiquidate(
address borrower,
uint256 seizedAssets, // Collateral to seize (0 to calculate from repaidShares)
uint256 repaidShares, // Debt shares to repay (0 to calculate from seizedAssets)
bytes calldata data // Callback data (empty for no callback)
) external returns (uint256 seizedAssets, uint256 repaidAssets);

The borrower parameter is the address of the MorphoBorrowPosition contract itself (the position that holds collateral on Morpho Blue).

You must specify exactly one of seizedAssets or repaidShares:

seizedAssets repaidShares Behavior
> 0 0 Specify the collateral amount to seize. Debt repayment is calculated proportionally.
0 > 0 Specify the debt shares to repay. Collateral seizure is calculated proportionally.
> 0 > 0 Reverts.
0 0 Reverts.
  • seizedAssets — the actual amount of collateral seized
  • repaidAssets — the actual amount of debt assets repaid (not shares)

If the data parameter is non-empty, the contract invokes a callback on msg.sender after transferring collateral but before pulling debt repayment:

interface IPreLiquidationCallback {
function onPreLiquidate(uint256 repaidAssets, bytes calldata data) external;
}
  1. Interest is accrued, then position health is checked — the proportional path requires the LTV to be above the liquidation LTV
  2. Collateral is transferred to the liquidator (msg.sender)
  3. Callback is invoked — onPreLiquidate(repaidAssets, data) is called on msg.sender
  4. Debt repayment is pulled from the liquidator

This ordering enables flash-liquidation patterns: you receive the collateral first, use the callback to sell it for the debt asset (e.g., via a DEX swap), then the contract pulls the debt repayment from your balance. No upfront capital is required.

A minimal liquidation bot contract would implement the callback like this:

contract FlashLiquidator is IPreLiquidationCallback {
function liquidate(
address position,
uint256 seizedAssets
) external {
IMorphoBorrowPosition(position).preLiquidate(
position, // borrower
seizedAssets, // collateral to seize
0, // calculate debt from collateral
abi.encode(position) // trigger callback
);
}
function onPreLiquidate(
uint256 repaidAssets,
bytes calldata data
) external {
address position = abi.decode(data, (address));
// 1. Collateral is already in this contract
// 2. Swap collateral → debt asset via DEX
// 3. Approve debt asset for the calling position
// ... swap and approve logic here ...
}
}

To find pre-liquidatable positions, monitor all deployed MorphoBorrowPosition contracts and check their health:

View Function Use
totalBorrowed() Current debt including accrued interest
totalCollateral() Current collateral in the position
totalCollateralQuoted() Collateral value in debt asset terms
isHealthy(ltv) Whether the position is at or below a given LTV threshold
maxBorrow(ltv) Maximum borrowable at a given LTV threshold
safeLtv() / liquidationLtv() The position’s two band thresholds

A position is proportionally pre-liquidatable when isHealthy(liquidationLtv()) returns false, and is in the offer band when that returns true while isHealthy(safeLtv()) returns false. Track BorrowPositionCreated events from the MorphoBorrowPositionFactory to discover positions, then read safeLtv() and liquidationLtv() on each.