Files
assetx/ytLending清算文档-final.txt
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

478 lines
17 KiB
Plaintext
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.

ytLending清算文档
重要说明
本清算机器人采用无状态轮询设计,通过监听链上事件发现活跃借款人,并自动执行清算操作以保护协议免受坏账风险。
核心特性:
- 无状态设计:无需数据库,所有数据从链上实时查询
- 轮询模式:每 5 秒检查一次新区块
- 事件驱动:监听 4 种事件发现活跃借款人
- 批量清算:一次交易清算多个账户,节省 Gas
目录
1.[系统启动流程](#1-系统启动流程)
2.[主循环轮询流程](#2-主循环轮询流程)
3.[活跃地址获取流程](#3-活跃地址获取流程)
4.[清算检查流程](#4-清算检查流程)
5.[批量清算执行流程](#5-批量清算执行流程)
6.[重要参数说明](#6-重要参数说明)
7.[完整脚本](#7-完整脚本)
8.[运行和部署](#8-运行和部署)
1. 系统启动流程
启动命令:
Bash
npx hardhat run scripts/liquidation_bot/index.ts --network arbSepolia
启动步骤:
步骤 1 — 初始化 Hardhat 环境,读取网络配置
Plain Text
network: arbSepolia
chainId: 421614
rpcUrl: https://arbitrum-sepolia.gateway.tenderly.co
步骤 2 — 读取部署信息(`deployments-lending.json`),加载合约地址
Plain Text
Lending Proxy: 0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D
Base Token (USDC): 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
Collateral Assets:
YT-A: 0x97204190B35D9895a7a47aa7BaC61ac08De3cF05
YT-B: 0x181ef4011c35C4a2Fda08eBC5Cf509Ef58E553fF
YT-C: 0xE9A5b9f3a2Eda4358f81d4E2eF4f3280A664e5B0
Price Feed 合约不再需要初始化
步骤 3 — 初始化签名者(从 `PRIVATE_KEY` 环境变量读取)
Plain Text
Liquidator Address: 0x...
ETH Balance: 0.5 ETH
要求:
- 需要 ETH 支付 Gas 费用
- 建议保持至少 0.1 ETH 余额
步骤 4 — 初始化合约实例
TypeScript
lendingContract = await ethers.getContractAt(
'Lending',
deployment.lendingProxy,
signer
)
步骤 5 — 进入主循环
2. 主循环轮询流程
主循环参数LOOP_DELAY = 5000ms5 秒)
循环步骤:
1.调用 provider.getBlockNumber() 获取当前区块号
2.判断是否为新区块:
- 新区块 → 执行清算逻辑(见下方步骤 3
- 已检查过 → 等待 5 秒后继续
3.执行 liquidateUnderwaterBorrowers(lendingContract, signer)
○① 获取活跃地址(查询事件)
○② 检查每个地址是否可清算
○③ 批量执行清算
4.更新 lastBlockNumber = currentBlockNumber循环回步骤 1
容错机制:
TypeScript
try {
// 执行清算逻辑
} catch (error) {
console.error('Error in main loop:', error)
// 继续运行,不中断
}
// 无论成功或失败,都等待 5 秒后继续
await sleep(5000)
关键设计决策:
参数 值 说明
LOOP_DELAY 5000ms 轮询间隔
去重检查 lastBlockNumber 每个区块只处理一次,避免重复
容错机制 try-catch 异常不中断,继续运行
3. 活跃地址获取流程
函数getUniqueAddresses(lendingContract)LOOKBACK_BLOCKS = 10000
步骤 1 — 计算查询区块范围
Plain Text
currentBlock = 12345678
fromBlock = 12345678 - 10000 = 12335678
toBlock = 12345678
时间窗口Arbitrum Sepolia出块 ~1 秒):
10000 区块 ≈ 2.7 小时
步骤 2 — 查询 4 种事件,提取活跃地址
事件 说明 提取字段
Withdraw 用户借款/提取 USDC event.args.src、event.args.dst
Supply 用户存入 USDC event.args.from、event.args.dst
SupplyCollateral 用户存入抵押品 event.args.from、event.args.dst
WithdrawCollateral 用户提取抵押品 event.args.src、event.args.to
步骤 3 — 合并去重
使用 Set<string> 自动去重,示例:
Plain Text
总事件数: 150 + 80 + 120 + 90 = 440 个
唯一地址数: 85 个(去重后)
覆盖率对比:
Compound V3仅 Withdraw: ~95%
本机器人4 种事件): ~98%
步骤 4 — 返回地址列表,进入清算检查
4. 清算检查流程
遍历所有活跃地址,对每个地址执行:
1.调用 lendingContract.isLiquidatable(address)(链上计算)
2.结果判断:
- false → 跳过(账户健康)
- true → 加入清算列表 `liquidatableAccounts[]`
3.继续检查下一个地址,全部检查完毕后进入批量清算
5. 批量清算执行流程
清算列表示例: `[0xAlice, 0xBob, 0xCharlie]`,共 3 个可清算账户
步骤 1 — 检查列表是否为空
•空列表 → 无清算,等待下一轮
•非空 → 继续执行
**步骤 2 — 调用 `absorbMultiple()`**
Solidity
function absorbMultiple(
address absorber, // 清算者地址(机器人)
address[] accounts // [0xAlice, 0xBob, 0xCharlie]
) external override nonReentrant
// 修饰符检查:
// ✓ nonReentrant - 防重入保护
// ✓ 未暂停检查
步骤 3 — 累积利息(`accrueInternal()`
•更新 supplyIndex存款利息
•更新 borrowIndex借款利息
•确保清算时使用最新的债务余额
**步骤 4 — 对每个账户执行 `_absorbInternal()`**
以 Alice 为例:
1.再次验证可清算性if (!isLiquidatable(Alice)) revert NotLiquidatable()(防止抢跑)
2.获取用户债务principal = userBasic[Alice].principal负数 = 借款,如 -1000e6
3.遍历所有抵押资产 [YT-A, YT-B, YT-C],获取抵押数量
4.计算抵押品价值collateralValue = collateralAmount × assetPrice × liquidationFactor(0.95)
5.转移抵押品userCollateral[Alice][asset] = 0userCollateral[absorber][asset] += collateralAmount
6.清零债务userBasic[Alice].principal = 0totalBorrowBase -= 1000e6
结果: Alice 债务清零,抵押品转移给清算者,清算者获得 5% 折扣
步骤 5 — 继续清算 Bob、Charlie相同流程
步骤 6 — 发送交易并等待确认
TypeScript
const tx = await lendingContract.absorbMultiple(
await signer.getAddress(), // 清算者地址
liquidatableAccounts // [Alice, Bob, Charlie]
)
const receipt = await tx.wait()
清算结果示例:
Plain Text
✅ Liquidation successful!
Transaction: 0xabc123...
Gas used: 450,000
Block: 12345679
清算了 3 个账户:
0xAlice: $1,000 债务
0xBob: $500 债务
0xCharlie: $2,000 债务
清算者获得的抵押品:
YT-A: 500 个(估值 $1,750
YT-B: 800 个(估值 $1,600
总价值: $3,350覆盖 $3,500 债务5% 折扣)
6. 重要参数说明
6.1 轮询参数
参数名 值 单位 说明 调优建议
LOOP_DELAY 5000 毫秒 轮询间隔 测试网 5000ms / 主网高活跃 2000ms / 激进模式 1000ms注意 RPC 限流)
LOOKBACK_BLOCKS 10000 区块 事件查询范围 Arbitrum 测试网 50000~14小时/ Arbitrum 主网 10000-20000~3-6小时/ 以太坊主网 10000~33小时
时间窗口参考:
Plain Text
Arbitrum出块 ~1 秒):
10000 区块 ≈ 2.7 小时
50000 区块 ≈ 14 小时
100000 区块 ≈ 28 小时
Ethereum 主网(出块 ~12 秒):
10000 区块 ≈ 33 小时
6.2 合约参数(只读)
参数名 典型值 精度 说明 位置
borrowCollateralFactor 0.80 18 decimals 借款抵押率80% LTV assetConfig
liquidateCollateralFactor 0.85 18 decimals 清算触发阈值85% assetConfig
liquidationFactor 0.95 18 decimals 清算激励因子5% 折扣) 全局配置
storeFrontPriceFactor 0.50 18 decimals buyCollateral 折扣系数 全局配置
三个抵押率的关系:
Plain Text
正常借款 → 接近清算 → 触发清算 → 清算完成
80% 83% 85% 清零
(借款) (预警) (清算) (债务)
健康因子计算:
Plain Text
healthFactor = (collateralValue × 0.85) / debtValue
healthFactor > 1.0:安全
healthFactor < 1.0:可清算
6.3 监听事件类型
事件名 触发场景 提取字段 说明
Withdraw 用户借款/提取 USDC src, dst 最主要的借款信号
Supply 用户存入 USDC from, dst 可能有借款人还款
SupplyCollateral 用户存入抵押品 from, dst 借款前的准备动作
WithdrawCollateral 用户提取抵押品 src, to 可能触发清算条件
6.4 日志示例
Plain Text
[2025-01-06T10:30:15.000Z] Block: 12345678
📊 Querying events from block 12335678 to 12345678...
- Withdraw events: 150
- Supply events: 80
- SupplyCollateral events: 120
- WithdrawCollateral events: 90
✅ Found 85 unique addresses from all events
🔍 Checking 85 addresses for liquidation...
💰 Liquidatable: 0xAlice...1234
💰 Liquidatable: 0xBob...5678
💰 Liquidatable: 0xEve...9012
🎯 Found 3 liquidatable accounts
📤 Sending liquidation transaction...
🔗 Transaction sent: 0xabc123...
✅ Liquidation successful!
Gas used: 450000
Block: 12345679
7. 完整脚本
index.ts
TypeScript
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(' 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
);
console.log('✅ Contracts initialized\n');
console.log('==========================================');
console.log('🔄 Starting main loop...\n');
let lastBlockNumber: number | undefined;
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, 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);
});
liquidateUnderwaterBorrowers.ts
TypeScript
import hre from 'hardhat';
import { Signer } from 'ethers';
const LOOKBACK_BLOCKS = 10000;
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>();
try {
const withdrawEvents = await lendingContract.queryFilter(
lendingContract.filters.Withdraw(), 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);
}
try {
const supplyEvents = await lendingContract.queryFilter(
lendingContract.filters.Supply(), 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);
}
try {
const supplyCollateralEvents = await lendingContract.queryFilter(
lendingContract.filters.SupplyCollateral(), 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);
}
try {
const withdrawCollateralEvents = await lendingContract.queryFilter(
lendingContract.filters.WithdrawCollateral(), 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,
signer: Signer
): Promise<boolean> {
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[] = [];
for (const address of uniqueAddresses) {
try {
const isLiquidatable = await lendingContract.isLiquidatable(address);
if (isLiquidatable) {
console.log(`💰 Liquidatable: ${address}`);
liquidatableAccounts.push(address);
}
} catch (error) {
console.error(`Error checking ${address}:`, error);
}
}
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;
}
}
8. 运行和部署
8.1 开发模式(前台运行)
Bash
npx hardhat run scripts/liquidation_bot/index.ts --network arbSepolia
8.2 生产部署PM2 后台运行)
Bash
# 1. 安装 PM2
npm install -g pm2
# 2. 启动清算机器人
pm2 start scripts/liquidation_bot/index.ts \
--name ytlp-liquidation-bot \
--interpreter npx \
--interpreter-args "hardhat run --network arbSepolia"
# 3. 查看日志
pm2 logs ytlp-liquidation-bot --lines 100
# 4. 保存配置(开机自启)
pm2 save
pm2 startup
# 5. 监控管理
pm2 list # 列出所有进程
pm2 restart ytlp-liquidation-bot # 重启
pm2 stop ytlp-liquidation-bot # 停止
pm2 delete ytlp-liquidation-bot # 删除
8.3 参数调优指南
场景 1测试网低活跃度
TypeScript
const LOOP_DELAY = 5000; // 5 秒(稳定)
const LOOKBACK_BLOCKS = 50000; // ~14 小时(覆盖更多历史)
场景 2主网高活跃度
TypeScript
const LOOP_DELAY = 2000; // 2 秒(快速响应)
const LOOKBACK_BLOCKS = 10000; // ~2.7 小时(用户活跃,足够)
总结
本清算机器人采用无状态轮询设计,通过监听多种事件提高覆盖率,对所有 isLiquidatable 为 true 的账户无金额限制地执行清算,确保及时清算水下账户,保护协议免受坏账风险。