Files
assetxContracts/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts
2026-01-06 17:31:16 +08:00

175 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import hre from 'hardhat';
import { Signer } from 'ethers';
const LOOKBACK_BLOCKS = 50000; // 查询最近 50000 个区块
const LIQUIDATION_THRESHOLD = 10; // $10 的最小清算阈值(美元单位)
/**
* 获取最近活跃的地址(通过多个事件)
*/
export async function getUniqueAddresses(
lendingContract: any
): Promise<string[]> {
const currentBlock = await hre.ethers.provider.getBlockNumber();
const fromBlock = Math.max(currentBlock - LOOKBACK_BLOCKS, 0);
console.log(`📊 Querying events from block ${fromBlock} to ${currentBlock}...`);
const uniqueAddresses = new Set<string>();
// 1. Withdraw 事件(借款/提现)
try {
const withdrawFilter = lendingContract.filters.Withdraw();
const withdrawEvents = await lendingContract.queryFilter(
withdrawFilter,
fromBlock,
currentBlock
);
for (const event of withdrawEvents) {
if (event.args?.src) uniqueAddresses.add(event.args.src);
if (event.args?.dst) uniqueAddresses.add(event.args.dst);
}
console.log(` - Withdraw events: ${withdrawEvents.length}`);
} catch (error) {
console.error(' ⚠️ Failed to query Withdraw events:', error);
}
// 2. Supply 事件(存款)
try {
const supplyFilter = lendingContract.filters.Supply();
const supplyEvents = await lendingContract.queryFilter(
supplyFilter,
fromBlock,
currentBlock
);
for (const event of supplyEvents) {
if (event.args?.from) uniqueAddresses.add(event.args.from);
if (event.args?.dst) uniqueAddresses.add(event.args.dst);
}
console.log(` - Supply events: ${supplyEvents.length}`);
} catch (error) {
console.error(' ⚠️ Failed to query Supply events:', error);
}
// 3. SupplyCollateral 事件(抵押品存入)
try {
const supplyCollateralFilter = lendingContract.filters.SupplyCollateral();
const supplyCollateralEvents = await lendingContract.queryFilter(
supplyCollateralFilter,
fromBlock,
currentBlock
);
for (const event of supplyCollateralEvents) {
if (event.args?.from) uniqueAddresses.add(event.args.from);
if (event.args?.dst) uniqueAddresses.add(event.args.dst);
}
console.log(` - SupplyCollateral events: ${supplyCollateralEvents.length}`);
} catch (error) {
console.error(' ⚠️ Failed to query SupplyCollateral events:', error);
}
// 4. WithdrawCollateral 事件(抵押品提取)
try {
const withdrawCollateralFilter = lendingContract.filters.WithdrawCollateral();
const withdrawCollateralEvents = await lendingContract.queryFilter(
withdrawCollateralFilter,
fromBlock,
currentBlock
);
for (const event of withdrawCollateralEvents) {
if (event.args?.src) uniqueAddresses.add(event.args.src);
if (event.args?.to) uniqueAddresses.add(event.args.to);
}
console.log(` - WithdrawCollateral events: ${withdrawCollateralEvents.length}`);
} catch (error) {
console.error(' ⚠️ Failed to query WithdrawCollateral events:', error);
}
console.log(`✅ Found ${uniqueAddresses.size} unique addresses from all events`);
return Array.from(uniqueAddresses);
}
/**
* 检查并清算可清算账户
*/
export async function liquidateUnderwaterBorrowers(
lendingContract: any,
priceFeedContract: any,
signer: Signer
): Promise<boolean> {
// 步骤 1: 获取最近活跃的地址
const uniqueAddresses = await getUniqueAddresses(lendingContract);
if (uniqueAddresses.length === 0) {
console.log(' No active addresses found');
return false;
}
console.log(`🔍 Checking ${uniqueAddresses.length} addresses for liquidation...`);
const liquidatableAccounts: string[] = [];
// 步骤 2: 检查每个地址是否可清算
for (const address of uniqueAddresses) {
try {
// 直接调用合约的 isLiquidatable(),无需自己计算健康因子
const isLiquidatable = await lendingContract.isLiquidatable(address);
if (isLiquidatable) {
// 应用清算阈值过滤防止清算小额账户gas 成本过高)
const borrowBalance = await lendingContract.borrowBalanceOf(address);
const baseToken = await lendingContract.baseToken();
const basePrice = await priceFeedContract.getPrice(baseToken);
// 动态获取 baseToken 的 decimals
const baseTokenContract = await hre.ethers.getContractAt('IERC20Metadata', baseToken);
const baseDecimals = await baseTokenContract.decimals();
// debtValue 计算borrowBalance * basePrice / 10^decimals
// 结果精度为 30 decimals (价格精度)
const debtValue = (BigInt(borrowBalance) * BigInt(basePrice)) / BigInt(10) ** BigInt(baseDecimals);
const debtValueInBaseUnit = Number(debtValue / (BigInt(10) ** BigInt(30))); // 转换为美元单位
// 将清算阈值(美元)转换为 30 decimals 精度进行比较
const thresholdValue = BigInt(LIQUIDATION_THRESHOLD) * BigInt(10) ** BigInt(30);
if (debtValue >= thresholdValue) {
console.log(`💰 Liquidatable: ${address}, Debt: $${debtValueInBaseUnit.toFixed(2)}`);
liquidatableAccounts.push(address);
} else {
console.log(`⏭️ Skip (below threshold): ${address}, Debt: $${debtValueInBaseUnit.toFixed(2)}`);
}
}
} catch (error) {
console.error(`Error checking ${address}:`, error);
}
}
// 步骤 3: 批量清算
if (liquidatableAccounts.length > 0) {
console.log(`\n🎯 Found ${liquidatableAccounts.length} liquidatable accounts`);
console.log('📤 Sending liquidation transaction...');
try {
const liquidatorAddress = await signer.getAddress();
const tx = await lendingContract.connect(signer).absorbMultiple(
liquidatorAddress,
liquidatableAccounts
);
console.log(`🔗 Transaction sent: ${tx.hash}`);
const receipt = await tx.wait();
console.log(`✅ Liquidation successful!`);
console.log(` Gas used: ${receipt.gasUsed.toString()}`);
console.log(` Block: ${receipt.blockNumber}`);
return true;
} catch (error) {
console.error('❌ Liquidation transaction failed:', error);
return false;
}
} else {
console.log('✅ No liquidatable accounts found');
return false;
}
}