add absorb script and upgrade script
This commit is contained in:
102
scripts/liquidation_bot/index.ts
Normal file
102
scripts/liquidation_bot/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import hre from 'hardhat';
|
||||
import { liquidateUnderwaterBorrowers } from './liquidateUnderwaterBorrowers';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
const LOOP_DELAY = 5000; // 5 秒轮询间隔
|
||||
|
||||
/**
|
||||
* 清算机器人主循环
|
||||
*/
|
||||
async function main() {
|
||||
const network = hre.network.name;
|
||||
const chainId = hre.network.config.chainId;
|
||||
|
||||
console.log('\n==========================================');
|
||||
console.log('🤖 YT Lending Liquidation Bot Started');
|
||||
console.log('==========================================');
|
||||
console.log('Network:', network);
|
||||
console.log('Chain ID:', chainId);
|
||||
console.log('Loop Delay:', LOOP_DELAY, 'ms\n');
|
||||
|
||||
// 读取部署信息
|
||||
const deploymentsPath = path.join(__dirname, '../../deployments-lending.json');
|
||||
if (!fs.existsSync(deploymentsPath)) {
|
||||
throw new Error('deployments-lending.json not found');
|
||||
}
|
||||
|
||||
const deployments = JSON.parse(fs.readFileSync(deploymentsPath, 'utf-8'));
|
||||
const deployment = deployments[chainId?.toString() || '421614'];
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(`No deployment found for chainId: ${chainId}`);
|
||||
}
|
||||
|
||||
console.log('📋 Contract Addresses:');
|
||||
console.log(' Lending Proxy:', deployment.lendingProxy);
|
||||
console.log(' Price Feed:', deployment.lendingPriceFeedProxy);
|
||||
console.log(' Base Token (USDC):', deployment.usdcAddress);
|
||||
console.log('');
|
||||
|
||||
// 获取签名者
|
||||
const [signer] = await hre.ethers.getSigners();
|
||||
console.log('👤 Liquidator Address:', await signer.getAddress());
|
||||
console.log('💰 Liquidator Balance:', hre.ethers.formatEther(await hre.ethers.provider.getBalance(signer)), 'ETH\n');
|
||||
|
||||
// 初始化合约
|
||||
const lendingContract = await hre.ethers.getContractAt(
|
||||
'Lending',
|
||||
deployment.lendingProxy,
|
||||
signer
|
||||
);
|
||||
|
||||
const priceFeedContract = await hre.ethers.getContractAt(
|
||||
'LendingPriceFeed',
|
||||
deployment.lendingPriceFeedProxy,
|
||||
signer
|
||||
);
|
||||
|
||||
console.log('✅ Contracts initialized\n');
|
||||
console.log('==========================================');
|
||||
console.log('🔄 Starting main loop...\n');
|
||||
|
||||
let lastBlockNumber: number | undefined;
|
||||
|
||||
// Compound V3 风格:while(true) 轮询
|
||||
while (true) {
|
||||
try {
|
||||
const currentBlockNumber = await hre.ethers.provider.getBlockNumber();
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Block: ${currentBlockNumber}`);
|
||||
|
||||
// 检查是否有新区块(每个区块只处理一次)
|
||||
if (currentBlockNumber !== lastBlockNumber) {
|
||||
lastBlockNumber = currentBlockNumber;
|
||||
|
||||
// 执行清算逻辑
|
||||
await liquidateUnderwaterBorrowers(
|
||||
lendingContract,
|
||||
priceFeedContract,
|
||||
signer
|
||||
);
|
||||
|
||||
console.log(''); // 空行分隔
|
||||
} else {
|
||||
console.log(`Block already checked; waiting ${LOOP_DELAY}ms...\n`);
|
||||
}
|
||||
|
||||
// 等待下一次轮询
|
||||
await new Promise(resolve => setTimeout(resolve, LOOP_DELAY));
|
||||
} catch (error) {
|
||||
console.error('❌ Error in main loop:', error);
|
||||
console.log(`Retrying in ${LOOP_DELAY}ms...\n`);
|
||||
await new Promise(resolve => setTimeout(resolve, LOOP_DELAY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error('❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
170
scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts
Normal file
170
scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import hre from 'hardhat';
|
||||
import { Signer } from 'ethers';
|
||||
|
||||
const LOOKBACK_BLOCKS = 50000; // 查询最近 50000 个区块
|
||||
const LIQUIDATION_THRESHOLD = 10e6; // $10 (USDC 6 decimals)
|
||||
|
||||
/**
|
||||
* 获取最近活跃的地址(通过多个事件)
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并清算可清算账户
|
||||
* 参考:comet/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts
|
||||
*/
|
||||
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);
|
||||
|
||||
// debtValue 计算:borrowBalance (6 decimals) * basePrice (30 decimals) / 1e6
|
||||
const debtValue = (BigInt(borrowBalance) * BigInt(basePrice)) / BigInt(10) ** BigInt(6);
|
||||
const debtValueInBaseUnit = Number(debtValue / (BigInt(10) ** BigInt(30))); // 转换为 USDC 单位
|
||||
|
||||
// LIQUIDATION_THRESHOLD 是 6 decimals,需要转换为 30 decimals 来和 debtValue 比较
|
||||
// 10e6 * 1e24 = 10e30 (代表 $10)
|
||||
if (debtValue >= BigInt(LIQUIDATION_THRESHOLD) * BigInt(10) ** BigInt(24)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user