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
andRepay
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
Fetch borrowers from borrow events or an indexer.
Check liquidity using
getAccountLiquidity
.Liquidate positions that have shortfalls.
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