Liquidation Bot

Build a Lotus liquidation bot that monitors positions, encodes lmData, executes liquidations, and uses flash-funded callbacks with profitability checks.

Build a bot that monitors Lotus positions, detects unhealthy borrowers, encodes lmData with BaseLiquidationParams, and executes liquidations.

Prerequisites

  • Understanding of liquidation mechanics (see Learn → Loan Health)

  • Understanding of BaseLiquidationParams encoding (see Build → Liquidation Modules)

  • RPC access to an Ethereum node

  • Familiarity with the liquidate function (see Reference → Contract API)

Step 1 — Monitor Positions

Identify all active borrowers by indexing Borrow events, then check each position's health:

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const lotus = new ethers.Contract(LOTUS_ADDRESS, lotusAbi, signer);

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

async function findUnhealthyPositions(marketId: string, marketParams: any) {
  const numTranches = await lotus.getNumMarketTranches(marketId);

  for (let t = 0; t < numTranches; t++) {
    // Use accrueInterestAndReturnMaxBorrow for accurate health checks
    const borrowers = await getActiveBorrowers(marketId, t); // from indexed events

    for (const borrower of borrowers) {
      const position = await lotus.getPosition(marketId, borrower);
      const borrowShares = position.borrowShares[t];
      if (borrowShares === 0n) continue;

      const tranches = await lotus.getMarketTranches(marketId);
      const tranche = tranches[t];
      const borrowed = borrowShares * tranche.trancheBorrowAssets / tranche.trancheBorrowShares;

      const oracle = new ethers.Contract(marketParams.oracles[t], oracleAbi, provider);
      const price = await oracle.price();
      const collateral = position.collateral[t];
      const collateralValue = collateral * price / ORACLE_PRICE_SCALE;
      const maxBorrow = collateralValue * marketParams.lltvs[t] / WAD;

      if (borrowed > maxBorrow) {
        console.log(`Unhealthy: ${borrower} tranche ${t}`);
        await executeLiquidation(marketParams, t, borrower, borrowShares, price);
      }
    }
  }
}

Step 2 — Encode lmData

The lmData parameter is decoded by the liquidation module's BaseLiquidationModule into a BaseLiquidationParams struct:

Exactly one of seizedAssets or repaidShares must be non-zero.

Asset-based mode — specify collateral to seize, the module computes repayment:

Share-based mode — specify borrow shares to repay, the module computes seizure:

In TypeScript:

Step 3 — Execute Liquidation

Call liquidate with the encoded lmData:

In TypeScript:

Step 4 — Flash-Funded Liquidation

You can liquidate without upfront capital using the liquidation callback. Pass callback data in BaseLiquidationParams to receive the ILotusLiquidateCallback:

Profitability Calculation

Walk through a concrete example:

Setup: A borrower has 10 WETH collateral and 15,000 USDC debt in a tranche with 90% liquidation loan-to-value (LLTV) and 1.05x liquidation incentive factor.

Price drop: ETH drops from $2,000 to $1,600. The borrower's loan-to-value (LTV) becomes 15,000 / (10 * 1,600) = 93.75%, exceeding the 90% LLTV.

Liquidation: The liquidator repays the full 15,000 USDC debt and seizes collateral worth 15,000 * 1.05 = 15,750 USDC at the oracle price.

Seized collateral: 15,750 / 1,600 = 9.84375 WETH

Profit calculation:

  • Seized: 9.84375 WETH (worth 15,750 USDC at oracle price)

  • Paid: 15,000 USDC

  • Gross profit: 750 USDC (the 5% incentive)

  • Minus: gas cost (~200K gas) and swap fees (~0.3%)

  • Net profit: ~700 USDC

The profitability depends on the incentive factor, gas price, swap routing efficiency, and competition from other liquidators.

See Also

Last updated