Building Liquidation Bots

Building Liquidation Bots on Fervent Finance

This page provides an overview for developers on how to interact with Fervent Finance smart contracts to detect and execute liquidations and calculate potential profits.

Understanding Liquidations

In Fervent Finance :

  • Borrowers maintain a health factor determined by the value of their collateral vs. borrowed assets.

  • If a borrower's health factor drops below 1, their position becomes liquidatable.

  • Liquidators can repay a portion of the borrower's debt in exchange for seizing collateral plus a liquidation incentive.

Finding Borrowers and Checking Liquidatability

1. Identify Borrowers

  • Event-based approach (on-chain): Listen to Borrow and Repay events from each cToken contract. Each event contains the borrower’s address.

// Example: Fetch past borrow events
const borrowEvents = await cToken.queryFilter(cToken.filters.Borrow());
const borrowers = [...new Set(borrowEvents.map(e => e.args.borrower))];
  • Off-chain/indexed approach: Use a subgraph or database tracking all borrowers per market. This is faster for large-scale monitoring.

2. Check if Borrowers are Liquidatable

Once you have a list of borrowers, call getAccountLiquidity for each one:

for (const borrower of borrowers) {
    const [error, liquidity, shortfall] = await comptroller.getAccountLiquidity(borrower);
    if (shortfall.gt(0)) {
        console.log("Liquidatable borrower:", borrower, "Shortfall:", shortfall.toString());
    }
}

Notes:

  • Using getAccountLiquidity handles multiple borrowed assets and price updates automatically.

  • It’s more reliable than manually calculating health factors.

  • For efficiency, you can batch requests or filter borrowers using an off-chain index.

3. Executing Liquidations

Once a borrower is liquidatable, call liquidateBorrow on the relevant cToken:

// Parameters: borrower address, repay amount, collateral cToken
await cToken.liquidateBorrow(borrower, repayAmount, cTokenCollateral);
console.log("Liquidation executed for borrower:", borrower);
  • Important:

    • Fervent Finance provides an 8% liquidation bonus.

    • Always factor in gas fees and maximum repayable amounts.

4. Calculating Profit

The profit from a liquidation comes from the seized collateral plus the 8% bonus, minus the debt repaid and gas costs:

const profit = (seizedCollateralValue * 1.08) - repaidDebtValue - gasCost;
console.log("Estimated profit:", profit);
  • Tips for maximizing profit:

    • Target borrowers with high collateral-to-debt ratios.

    • Consider market prices and slippage.

    • Monitor multiple borrowers to catch opportunities early.

Workflow Summary

  1. Fetch borrowers from borrow events or an indexer.

  2. Check liquidity using getAccountLiquidity.

  3. Liquidate positions that have shortfalls.

  4. Calculate profits using collateral seized and liquidation reward.

Example

const { ethers } = require('ethers');

class FerventLiquidationBot {
  constructor(providerUrl, privateKey, comptrollerAddress) {
    this.provider = new ethers.providers.JsonRpcProvider(providerUrl);
    this.wallet = new ethers.Wallet(privateKey, this.provider);
    this.comptroller = new ethers.Contract(comptrollerAddress, COMPTROLLER_ABI, this.wallet);
    this.cTokens = new Map();
    this.borrowers = new Set();
  }

  async initialize() {
    // Get all markets and initialize cToken contracts
    const markets = await this.comptroller.getAllMarkets();
    
    for (const marketAddress of markets) {
      const cToken = new ethers.Contract(marketAddress, CTOKEN_ABI, this.wallet);
      this.cTokens.set(marketAddress, cToken);
      
      // Listen for new borrow events
      cToken.on("Borrow", (borrower) => {
        this.borrowers.add(borrower);
        console.log("New borrower detected:", borrower);
      });
    }
    
    await this.scanExistingBorrowers();
    this.startMonitoring();
  }

  async scanExistingBorrowers() {
    // Scan recent borrow events to build initial borrower list
    for (const [, cToken] of this.cTokens) {
      const borrowEvents = await cToken.queryFilter(cToken.filters.Borrow(), -1000);
      borrowEvents.forEach(event => this.borrowers.add(event.args.borrower));
    }
    console.log(`Found ${this.borrowers.size} borrowers`);
  }

  startMonitoring() {
    // Check for liquidations every 30 seconds
    setInterval(async () => {
      await this.checkLiquidations();
    }, 30000);
  }

  async checkLiquidations() {
    for (const borrower of this.borrowers) {
      try {
        const [error, liquidity, shortfall] = await this.comptroller.getAccountLiquidity(borrower);
        
        if (shortfall.gt(0)) {
          console.log("Liquidatable borrower found:", borrower);
          await this.executeLiquidation(borrower);
        }
      } catch (error) {
        console.error("Error checking borrower:", error.message);
      }
    }
  }

  async executeLiquidation(borrowerAddress) {
    // Get borrower positions
    const positions = await this.getBorrowerPositions(borrowerAddress);
    
    if (positions.borrows.length === 0 || positions.supplies.length === 0) {
      return;
    }

    // Use largest borrow and supply for simplicity
    const borrowPosition = positions.borrows[0];
    const collateralPosition = positions.supplies[0];
    
    // Calculate liquidation amounts
    const repayAmount = borrowPosition.balance.div(2); // Max 50% of borrow
    const profit = await this.calculateProfit(borrowPosition, collateralPosition, repayAmount);
    
    if (profit.lt(ethers.parseEther("0.01"))) {
      console.log("Liquidation not profitable");
      return;
    }

    try {
      // Execute liquidation
      const tx = await borrowPosition.contract.liquidateBorrow(
        borrowerAddress,
        repayAmount,
        collateralPosition.contract.address
      );
      
      console.log("Liquidation executed:", tx.hash);
      await tx.wait();
      console.log("Liquidation confirmed");
      
    } catch (error) {
      console.error("Liquidation failed:", error.message);
    }
  }

  async getBorrowerPositions(borrowerAddress) {
    const positions = { borrows: [], supplies: [] };
    
    for (const [address, cToken] of this.cTokens) {
      // Check borrow balance
      const borrowBalance = await cToken.borrowBalanceStored(borrowerAddress);
      if (borrowBalance.gt(0)) {
        positions.borrows.push({
          contract: cToken,
          balance: borrowBalance,
          address: address
        });
      }
      
      // Check supply balance
      const supplyBalance = await cToken.balanceOf(borrowerAddress);
      if (supplyBalance.gt(0)) {
        positions.supplies.push({
          contract: cToken,
          balance: supplyBalance,
          address: address
        });
      }
    }
    
    return positions;
  }

  async calculateProfit(borrowPosition, collateralPosition, repayAmount) {
    // Simplified profit calculation
    // In practice, you'd get real prices from oracle
    const repayValue = repayAmount; // Assume 1:1 for simplicity
    const seizedValue = repayValue.mul(108).div(100); // 8% liquidation bonus
    const profit = seizedValue.sub(repayValue);
    
    return profit;
  }
}

// Contract ABIs (essential functions only)
const COMPTROLLER_ABI = [
  "function getAllMarkets() view returns (address[])",
  "function getAccountLiquidity(address) view returns (uint, uint, uint)"
];

const CTOKEN_ABI = [
  "function borrowBalanceStored(address) view returns (uint)",
  "function balanceOf(address) view returns (uint)",
  "function liquidateBorrow(address borrower, uint repayAmount, address cTokenCollateral) returns (uint)",
  "event Borrow(address borrower, uint borrowAmount, uint accountBorrows, uint totalBorrows)"
];

// Usage
async function main() {
  const bot = new FerventLiquidationBot(
    "https://your-rpc-endpoint.com",
    "0x1234567890abcdef...", // Your private key
    "0xComptrollerAddress"    // Fervent Finance comptroller
  );
  
  await bot.initialize();
  console.log("Liquidation bot started");
}

main().catch(console.error);

Last updated