Pre-Liquidation

Step-by-step guide to deploying pre-liquidation contracts, authorizing borrowers, and building monitoring bots with Solidity and TypeScript examples.

Deploy a pre-liquidation contract, authorize it for borrowers, and build a monitoring bot.

circle-info

Prerequisites: Understanding of pre-liquidation concepts (see Learn → Pre-Liquidation), familiarity with the PreLiquidation API (see Reference → Pre-Liquidation), and access to Lotus market parameters and tranche configuration.

triangle-exclamation
1

Choose Parameters

Before deploying, choose the six parameters that define the pre-liquidation behavior. The tradeoffs are:

  • PRE_LLTV: The LTV where pre-liquidation begins. Closer to LLTV means a narrower zone and less protection for borrowers. A wider gap gives borrowers more runway but means pre-liquidation kicks in earlier.

  • PRE_LCF_1 / PRE_LCF_2: The close factor range. PRE_LCF_1 is the minimum close factor at PRE_LLTV; PRE_LCF_2 is the maximum at LLTV. Higher values mean more aggressive pre-liquidation (more debt repaid per call). PRE_LCF_1 must be ≤ PRE_LCF_2 and PRE_LCF_1 must be ≤ 1e18.

  • PRE_LIF_1 / PRE_LIF_2: The incentive factor range. PRE_LIF_1 is the minimum incentive at PRE_LLTV (must be ≥ 1e18, i.e. at least 100%); PRE_LIF_2 is the maximum at LLTV (must be ≤ WAD.divWad(LLTV), i.e. 1e36 / LLTV). Higher incentives attract liquidators but cost borrowers more collateral.

  • Oracle: The oracle used for pre-liquidation price checks. Can be the same as the tranche's oracle or a faster-updating alternative.

Sensible starting values for a tranche with LLTV of 90%:

Parameter
Value
Meaning

preLltv

0.85e18

Pre-liquidation starts at 85% LTV

preLCF1

0.05e18

5% close factor at PRE_LLTV

preLCF2

0.20e18

20% close factor at LLTV

preLIF1

1.01e18

1% incentive at PRE_LLTV

preLIF2

1.05e18

5% incentive at LLTV

preLiquidationOracle

tranche oracle

Same oracle as the tranche

2

Deploy via Factory

Deploy a pre-liquidation contract using the PreLiquidationFactory. The factory uses CREATE2 for deterministic addresses.

import {PreLiquidationFactory} from "lotus/pre-liquidation/PreLiquidationFactory.sol";
import {IPreLiquidation} from "lotus/pre-liquidation/interfaces/IPreLiquidation.sol";

PreLiquidationParams memory params = PreLiquidationParams({
    preLltv: 0.85e18,
    preLCF1: 0.05e18,
    preLCF2: 0.20e18,
    preLIF1: 1.01e18,
    preLIF2: 1.05e18,
    preLiquidationOracle: oracleAddress
});

bytes32 salt = keccak256("my-pre-liquidation-v1");

IPreLiquidation preLiq = factory.createPreLiquidation(
    marketId,
    trancheIndex,
    params,
    salt
);

You can compute the deployment address before deploying:

address predictedAddress = factory.computePreLiquidationAddress(
    marketId,
    trancheIndex,
    params,
    salt
);

This is useful for allowing borrowers to authorize the contract before it exists on-chain.

3

Borrower Authorization

The borrower must authorize the pre-liquidation contract to withdraw collateral on their behalf. Without this, pre-liquidation calls will revert.

Direct authorization:

// Borrower calls this on the Lotus core contract
lotus.setAuthorization(address(preLiq), true);

EIP-712 signed authorization (gasless for borrower):

// Off-chain: borrower signs an EIP-712 typed message
Authorization memory auth = Authorization({
    authorizer: borrowerAddress,
    authorized: address(preLiq),
    isAuthorized: true,
    nonce: lotus.nonce(borrowerAddress),
    deadline: block.timestamp + 1 hours
});

// Borrower signs the typed data (e.g. via wallet)
(uint8 v, bytes32 r, bytes32 s) = vm.sign(borrowerKey, typedDataHash);

// Anyone can submit the signed authorization
lotus.setAuthorizationWithSig(auth, Signature({v: v, r: r, s: s}));

The authorization is protocol-wide. It allows the pre-liquidation contract to call withdrawCollateral on any of the borrower's positions. The pre-liquidation contract itself is scoped to a single market and tranche, so it can only act on the position it was deployed for. Repayment does not require authorization; anyone can repay on behalf of any borrower.

4

Monitor and Execute

The following TypeScript example shows how to build a bot that monitors positions and executes pre-liquidations:

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);

const preLiq = new ethers.Contract(preLiqAddress, preLiqAbi, signer);
const oracle = new ethers.Contract(oracleAddress, oracleAbi, provider);
const lotus = new ethers.Contract(lotusAddress, lotusAbi, provider);

const ORACLE_PRICE_SCALE = 10n ** 36n;
const WAD = 10n ** 18n;

async function checkAndPreLiquidate(borrower: string) {
  // 1. Get position and tranche state
  const position = await lotus.getPosition(marketId, borrower);
  const tranches = await lotus.getMarketTranches(marketId);
  const tranche = tranches[trancheIndex];

  const collateral = position.collateral[trancheIndex];
  const borrowShares = position.borrowShares[trancheIndex];
  if (collateral === 0n || borrowShares === 0n) return;

  // 2. Compute LTV using the pre-liquidation oracle
  const price = await oracle.price();
  const borrowed = borrowShares * tranche.trancheBorrowAssets / tranche.trancheBorrowShares;
  const collateralValue = collateral * price / ORACLE_PRICE_SCALE;
  const ltv = borrowed * WAD / collateralValue;

  const preLltv = await preLiq.PRE_LLTV();
  const lltv = await preLiq.LLTV();

  // 3. Check if in pre-liquidation zone
  if (ltv <= preLltv || ltv >= lltv) return;

  // 4. Compute close factor via interpolation
  const preLCF1 = await preLiq.PRE_LCF_1();
  const preLCF2 = await preLiq.PRE_LCF_2();
  const quotient = (ltv - preLltv) * WAD / (lltv - preLltv);
  const closeFactor = quotient * (preLCF2 - preLCF1) / WAD + preLCF1;

  // 5. Calculate repay amount
  const maxRepayShares = borrowShares * closeFactor / WAD;

  // 6. Execute pre-liquidation
  const tx = await preLiq.preLiquidate(borrower, 0, maxRepayShares, "0x");
  const receipt = await tx.wait();
  console.log(`Pre-liquidated ${borrower}: tx ${receipt.hash}`);
}

For flash-funded pre-liquidation (no upfront capital), implement IPreLiquidationCallback:

contract FlashPreLiquidator is IPreLiquidationCallback {
    function execute(address preLiq, address borrower, uint256 repaidShares) external {
        // Trigger pre-liquidation with callback data
        IPreLiquidation(preLiq).preLiquidate(borrower, 0, repaidShares, abi.encode(msg.sender));
    }

    function onPreLiquidate(uint256 repaidAssets, bytes calldata data) external {
        // At this point, collateral has been sent to this contract.
        // Swap collateral → loan token to cover repaidAssets.
        // The pre-liquidation contract will pull repaidAssets of loan token from us.
    }
}
5

Testing

A Forge test showing a complete pre-liquidation flow:

function test_preLiquidation() public {
    // Setup: create market, deposit collateral, borrow
    vm.prank(borrower);
    lotus.supplyCollateral(marketParams, trancheIndex, 10e18, borrower, "");

    vm.prank(borrower);
    lotus.borrow(marketParams, trancheIndex, 8000e6, 0, borrower, borrower, "");

    // Borrower authorizes pre-liquidation contract
    vm.prank(borrower);
    lotus.setAuthorization(address(preLiq), true);

    // Price drops → borrower enters pre-liquidation zone
    oracle.setPrice(newLowerPrice);

    // Liquidator executes pre-liquidation
    uint256 repaidShares = 100e6; // repay a portion of borrow shares
    vm.prank(liquidator);
    (uint256 seized, uint256 repaid) = preLiq.preLiquidate(borrower, 0, repaidShares, "");

    // Verify partial liquidation occurred
    assertGt(seized, 0, "collateral should be seized");
    assertGt(repaid, 0, "debt should be repaid");
}

See Also

Last updated