chore: 移除根目录合约/设计文档,不纳入版本管理

This commit is contained in:
2026-03-27 11:32:29 +00:00
parent 2ee4553b71
commit 7a21ef777c
10 changed files with 6 additions and 15429 deletions

6
.gitignore vendored
View File

@@ -34,5 +34,11 @@ tmp/
# Backup # Backup
backup/ backup/
# Root-level docs
*.txt
*.html
INTERNSHIP_EXPERIENCE.md
system-panorama.html
# Claude config (local) # Claude config (local)
.claude/ .claude/

View File

@@ -1,528 +0,0 @@
holder获取方案文档
ytLp 协议 Holder 获取方案
本文档详细说明如何获取 ytLp 协议中三种角色的持有者Holder信息YT 代币持有者、LP 代币持有者和 Lending 提供者。
目录
1.[YT 代币持有者](#1-yt-代币持有者)
2.[LP 代币持有者ytLP](#2-lp-代币持有者ytlp)
3.[Lending 提供者](#3-lending-提供者)
4.[完整解决方案](#3-完整解决方案示例)
1. YT 代币持有者
合约信息
合约: `YTAssetVault.sol`
位置: `contracts/ytVault/YTAssetVault.sol`
标准: ERC20继承自 `ERC20Upgradeable`
已部署的 YT 代币
代币名称 合约地址
YT-A 0x03d2a3B21238CD65D92c30A81b3f80d8bA1A44AC
YT-B 0xf41fc97d8a3c9006Dd50Afa04d0a3D6D27f8cD0B
YT-C 0xBdE54f062C537CA7D96Cd97e5B827918E38E97b5
获取方式
监听事件
YT 代币作为标准 ERC20 代币,会发出以下事件:
标准 ERC20 Transfer 事件:
Solidity
event Transfer(address indexed from, address indexed to, uint256 value);
获取步骤:
1.监听 Transfer 事件,收集所有接收过代币的地址
2.过滤掉零地址 0x0000...0000(铸造)和合约地址本身(销毁)
3.对于每个地址,调用 balanceOf(address) 查询当前余额
4.余额 > 0 的地址即为持有者
2. LP 代币持有者ytLP
合约信息
合约: `YTLPToken.sol`
位置: `contracts/ytLp/tokens/YTLPToken.sol`
标准: ERC20继承自 `ERC20Upgradeable`
已部署地址: `0x102e3F25Ef0ad9b0695C8F2daF8A1262437eEfc3`
获取方式
监听事件
标准 ERC20 Transfer 事件:
Solidity
event Transfer(address indexed from, address indexed to, uint256 value);
获取步骤:
1.监听 ytLP 代币合约的 Transfer 事件
2.收集所有接收过代币的地址to 参数)
3.过滤零地址和合约地址
4.调用 balanceOf(address) 获取当前余额
5.余额 > 0 的即为 LP 持有者
3. Lending 提供者
合约信息
合约: `Lending.sol`
位置: `contracts/ytLending/Lending.sol`
已部署地址: `0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D`
特点: 非 ERC20 代币,通过 mapping 存储用户余额
数据存储结构
Solidity
// 用户基本信息(在 LendingStorage.sol 中定义)
struct UserBasic {
int104 principal; // 本金(正数=存款本金,负数=借款本金)
}
mapping(address => UserBasic) public userBasic;
// 用户抵押品余额
mapping(address => mapping(address => uint256)) public userCollateral;
获取方式
监听事件
Lending 相关事件 (定义在 `ILending.sol`):
Solidity
event Supply(address indexed from, address indexed dst, uint256 amount);
event Withdraw(address indexed src, address indexed to, uint256 amount);
event SupplyCollateral(address indexed from, address indexed dst, address indexed asset, uint256 amount);
event WithdrawCollateral(address indexed src, address indexed to, address indexed asset, uint256 amount);
event AbsorbDebt(address indexed absorber, address indexed borrower, uint256 basePaidOut, uint256 usdValue);
event AbsorbCollateral(address indexed absorber, address indexed borrower, address indexed asset, uint256 collateralAbsorbed, uint256 usdValue);
event BuyCollateral(address indexed buyer, address indexed asset, uint256 baseAmount, uint256 collateralAmount);
获取 USDC 提供者步骤:
1.监听 Supply 事件,收集所有曾经提供 USDC 的地址dst 参数)
2.监听 Withdraw 事件,跟踪提现行为
3.对每个地址调用 supplyBalanceOf(address) 查询当前供应余额
4.供应余额 > 0 的即为当前的 Lending 提供者
获取抵押品提供者步骤:
1.监听 SupplyCollateral 事件,收集提供抵押品的地址
2.对每个地址和资产调用 getCollateral(address, asset) 查询余额
3.抵押品余额 > 0 的即为抵押品提供者
余额说明:
•principal > 0: 用户是供应者(存款方)
•principal < 0: 用户是借款者
•principal = 0: 用户无供应也无借款
•实际余额需要通过 supplyBalanceOf() 或 borrowBalanceOf() 获取(含利息累计)
注意事项
1. 利息累计: Lending 使用复合式利息,余额会随时间增长
•principal 是存储的本金(不变)
•supplyBalanceOf() 返回实时余额(含利息)
•需要调用 accrueInterest() 或读取函数自动累计利息
2. 借款和供应同时存在:
•一个地址可能既是供应者又是借款者
•principal > 0: 净供应者
•principal < 0: 净借款者
3. 抵押品种类:
•通过 assetList() 获取支持的抵押资产列表
•对每个资产调用 getCollateral(user, asset) 查询余额
4. 完整解决方案示例
以下是一个完整的 TypeScript 脚本,可以获取所有三种 holder
从合约部署区块开始扫描扫描到当前区块后记录扫描最终区块每隔10s从上次记录的区块扫描到最新区块以此类推存储到数据库自行加代码。
TypeScript
import { ethers, Contract, JsonRpcProvider } from "ethers";
import type { EventLog, Log } from "ethers";
// ==================== 类型定义 ====================
interface VaultConfig {
name: string;
address: string;
}
interface YTHolderData {
address: string;
balance: string;
}
interface LPHolderData {
address: string;
balance: string;
share: string;
}
interface LendingSupplierData {
address: string;
supply: string;
borrow: string;
net: string;
}
// ==================== 配置 ====================
const RPC_URL: string = "https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07";
// 合约配置(包含部署区块号,可以大幅减少查询时间)
const YT_VAULTS: VaultConfig[] = [
{ name: "YT-A", address: "0x97204190B35D9895a7a47aa7BaC61ac08De3cF05" },
{ name: "YT-B", address: "0x181ef4011c35C4a2Fda08eBC5Cf509Ef58E553fF" },
{ name: "YT-C", address: "0xE9A5b9f3a2Eda4358f81d4E2eF4f3280A664e5B0" },
];
const YTLP_ADDRESS: string = "0x102e3F25Ef0ad9b0695C8F2daF8A1262437eEfc3";
const LENDING_ADDRESS: string = "0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D";
// ==================== 部署区块配置 ====================
//
// 配置说明:
// 1. 查询准确的部署区块号,直接填写
interface DeploymentConfig {
ytVaults: number; // YT 代币部署区块
ytlp: number; // ytLP 部署区块
lending: number; // Lending 部署区块
}
const DEPLOYMENT_BLOCKS: DeploymentConfig = {
ytVaults: 227339300, // YT-A/B/C 部署区块号
ytlp: 227230270, // ytLP 部署区块号
lending: 227746053, // Lending 部署区块号
};
// ==================== ABIs ====================
const ERC20_ABI = [
"event Transfer(address indexed from, address indexed to, uint256 value)",
"function balanceOf(address account) view returns (uint256)",
"function totalSupply() view returns (uint256)",
] as const;
const LENDING_ABI = [
"event Supply(address indexed from, address indexed dst, uint256 amount)",
"function supplyBalanceOf(address account) view returns (uint256)",
"function borrowBalanceOf(address account) view returns (uint256)",
] as const;
// ==================== 工具函数 ====================
/**
* 分块查询事件,避免超出 RPC 限制
* @param contract 合约实例
* @param filter 事件过滤器
* @param fromBlock 起始区块
* @param toBlock 结束区块
* @param batchSize 每批次的区块数量(默认 9999低于 10000 限制)
*/
async function queryEventsInBatches(
contract: Contract,
filter: any,
fromBlock: number,
toBlock: number,
batchSize: number = 9999
): Promise<(EventLog | Log)[]> {
const allEvents: (EventLog | Log)[] = [];
let currentBlock = fromBlock;
console.log(` 查询区块范围: ${fromBlock} -> ${toBlock} (总共 ${toBlock - fromBlock + 1} 个区块)`);
while (currentBlock <= toBlock) {
const endBlock = Math.min(currentBlock + batchSize, toBlock);
console.log(` 正在查询区块 ${currentBlock} - ${endBlock}...`);
try {
const events = await contract.queryFilter(filter, currentBlock, endBlock);
allEvents.push(...events);
console.log(` ✓ 获取到 ${events.length} 个事件`);
} catch (error) {
console.error(` ✗ 查询区块 ${currentBlock} - ${endBlock} 失败:`, error);
throw error;
}
currentBlock = endBlock + 1;
// 添加小延迟,避免触发 RPC 速率限制
if (currentBlock <= toBlock) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
console.log(` 总计获取 ${allEvents.length} 个事件\n`);
return allEvents;
}
/**
* 获取当前最新区块号
*/
async function getLatestBlockNumber(provider: JsonRpcProvider, silent: boolean = false): Promise<number> {
const blockNumber = await provider.getBlockNumber();
if (!silent) {
console.log(`当前最新区块: ${blockNumber}\n`);
}
return blockNumber;
}
// ==================== 主函数 ====================
// 记录上次扫描的区块号
let lastScannedBlock: number = 0;
// 标记是否正在扫描,防止并发
let isScanning: boolean = false;
// 全局地址集合,用于追踪所有曾经出现过的地址
const allYTAddresses: Map<string, Set<string>> = new Map(); // vault address -> holder addresses
const allLPAddresses: Set<string> = new Set();
const allLendingAddresses: Set<string> = new Set();
async function getAllHolders(
provider: JsonRpcProvider,
fromBlock?: number,
toBlock?: number,
isInitialScan: boolean = false
): Promise<void> {
// 获取最新区块号
const latestBlock = toBlock || await getLatestBlockNumber(provider, fromBlock !== undefined);
// 计算起始区块
let ytVaultsStartBlock: number;
let ytlpStartBlock: number;
let lendingStartBlock: number;
if (fromBlock !== undefined) {
// 增量扫描模式
ytVaultsStartBlock = ytlpStartBlock = lendingStartBlock = fromBlock;
console.log(`\n🔄 增量扫描: 区块 ${fromBlock} -> ${latestBlock}\n`);
} else {
// 首次扫描:使用部署区块号
ytVaultsStartBlock = DEPLOYMENT_BLOCKS.ytVaults;
ytlpStartBlock = DEPLOYMENT_BLOCKS.ytlp;
lendingStartBlock = DEPLOYMENT_BLOCKS.lending;
if (isInitialScan) {
console.log(`✨ 首次扫描,从部署区块开始:`);
console.log(` YT Vaults 起始区块: ${ytVaultsStartBlock}`);
console.log(` ytLP 起始区块: ${ytlpStartBlock}`);
console.log(` Lending 起始区块: ${lendingStartBlock}\n`);
}
}
// 1. 获取 YT 代币持有者
console.log("1. YT 代币持有者:");
for (const vault of YT_VAULTS) {
console.log(` 正在查询 ${vault.name} (${vault.address})...`);
const contract: Contract = new ethers.Contract(vault.address, ERC20_ABI, provider);
const filter = contract.filters.Transfer();
const events: (EventLog | Log)[] = await queryEventsInBatches(
contract,
filter,
ytVaultsStartBlock,
latestBlock
);
// 初始化该 vault 的地址集合(如果不存在)
if (!allYTAddresses.has(vault.address)) {
allYTAddresses.set(vault.address, new Set<string>());
}
const vaultAddresses = allYTAddresses.get(vault.address)!;
// 记录新增地址数量
const previousCount = vaultAddresses.size;
// 添加新发现的地址到全局集合
for (const event of events) {
if ("args" in event && event.args.to !== ethers.ZeroAddress) {
vaultAddresses.add(event.args.to as string);
}
}
const newAddressCount = vaultAddresses.size - previousCount;
if (newAddressCount > 0) {
console.log(` 发现 ${newAddressCount} 个新地址,总共追踪 ${vaultAddresses.size} 个地址`);
}
// 查询所有曾经出现过的地址的当前余额
const holders: YTHolderData[] = [];
for (const address of vaultAddresses) {
const balance: bigint = await contract.balanceOf(address);
if (balance > 0n) {
holders.push({
address,
balance: ethers.formatEther(balance),
});
}
}
// 按余额降序排序
holders.sort((a, b) => parseFloat(b.balance) - parseFloat(a.balance));
console.log(` ${vault.name}: ${holders.length} 持有者`);
if (holders.length > 0) {
console.log(` 前 10 名持有者:`);
const top10 = holders.slice(0, 10);
top10.forEach((h: YTHolderData, index: number) =>
console.log(` ${index + 1}. ${h.address}: ${h.balance}`)
);
} else {
console.log(` 暂无持有者`);
}
}
// 2. 获取 LP 代币持有者
console.log("\n2. LP 代币持有者 (ytLP):");
console.log(` 正在查询 ytLP (${YTLP_ADDRESS})...`);
const lpContract: Contract = new ethers.Contract(YTLP_ADDRESS, ERC20_ABI, provider);
const lpFilter = lpContract.filters.Transfer();
const lpEvents: (EventLog | Log)[] = await queryEventsInBatches(
lpContract,
lpFilter,
ytlpStartBlock,
latestBlock
);
// 记录新增地址数量
const previousLPCount = allLPAddresses.size;
// 添加新发现的地址到全局集合
for (const event of lpEvents) {
if ("args" in event && event.args.to !== ethers.ZeroAddress) {
allLPAddresses.add(event.args.to as string);
}
}
const newLPAddressCount = allLPAddresses.size - previousLPCount;
if (newLPAddressCount > 0) {
console.log(` 发现 ${newLPAddressCount} 个新地址,总共追踪 ${allLPAddresses.size} 个地址`);
}
// 查询所有曾经出现过的地址的当前余额
const lpHolders: LPHolderData[] = [];
const totalSupply: bigint = await lpContract.totalSupply();
for (const address of allLPAddresses) {
const balance: bigint = await lpContract.balanceOf(address);
if (balance > 0n) {
const share: string = (Number(balance) / Number(totalSupply) * 100).toFixed(4);
lpHolders.push({
address,
balance: ethers.formatEther(balance),
share: share + "%",
});
}
}
// 按余额降序排序
lpHolders.sort((a, b) => parseFloat(b.balance) - parseFloat(a.balance));
console.log(` 总计: ${lpHolders.length} 持有者`);
if (lpHolders.length > 0) {
console.log(` 前 10 名持有者:`);
const top10 = lpHolders.slice(0, 10);
top10.forEach((h: LPHolderData, index: number) =>
console.log(` ${index + 1}. ${h.address}: ${h.balance} (${h.share})`)
);
} else {
console.log(` 暂无持有者`);
}
// 3. 获取 Lending 提供者
console.log("\n3. Lending 提供者:");
console.log(` 正在查询 Lending (${LENDING_ADDRESS})...`);
const lendingContract: Contract = new ethers.Contract(LENDING_ADDRESS, LENDING_ABI, provider);
const supplyFilter = lendingContract.filters.Supply();
const supplyEvents: (EventLog | Log)[] = await queryEventsInBatches(
lendingContract,
supplyFilter,
lendingStartBlock,
latestBlock
);
// 记录新增地址数量
const previousLendingCount = allLendingAddresses.size;
// 添加新发现的地址到全局集合
for (const event of supplyEvents) {
if ("args" in event) {
allLendingAddresses.add(event.args.dst as string);
}
}
const newLendingAddressCount = allLendingAddresses.size - previousLendingCount;
if (newLendingAddressCount > 0) {
console.log(` 发现 ${newLendingAddressCount} 个新地址,总共追踪 ${allLendingAddresses.size} 个地址`);
}
// 查询所有曾经出现过的地址的当前余额
const suppliers: LendingSupplierData[] = [];
for (const address of allLendingAddresses) {
const supplyBalance: bigint = await lendingContract.supplyBalanceOf(address);
const borrowBalance: bigint = await lendingContract.borrowBalanceOf(address);
if (supplyBalance > 0n || borrowBalance > 0n) {
suppliers.push({
address,
supply: ethers.formatUnits(supplyBalance, 6),
borrow: ethers.formatUnits(borrowBalance, 6),
net: ethers.formatUnits(supplyBalance - borrowBalance, 6),
});
}
}
// 按净供应额降序排序
suppliers.sort((a, b) => parseFloat(b.net) - parseFloat(a.net));
console.log(` 总计: ${suppliers.length} 参与者`);
if (suppliers.length > 0) {
console.log(` 前 10 名参与者:`);
const top10 = suppliers.slice(0, 10);
top10.forEach((s: LendingSupplierData, index: number) =>
console.log(
` ${index + 1}. ${s.address}: 供应=${s.supply} USDC, 借款=${s.borrow} USDC, 净额=${s.net} USDC`
)
);
} else {
console.log(` 暂无参与者`);
}
// 更新上次扫描的区块号
lastScannedBlock = latestBlock;
console.log(`\n📌 已记录扫描区块: ${lastScannedBlock}`);
}
// ==================== 执行 ====================
const POLL_INTERVAL_MS = 10000; // 10秒轮询间隔
async function main(): Promise<void> {
const provider: JsonRpcProvider = new ethers.JsonRpcProvider(RPC_URL);
console.log("=== ytLp 协议 Holder 数据监控 ===\n");
console.log(`轮询间隔: ${POLL_INTERVAL_MS / 1000} 秒\n`);
try {
// 首次扫描:从部署区块到当前区块
console.log("📊 开始首次扫描...\n");
const startTime = Date.now();
await getAllHolders(provider, undefined, undefined, true);
const endTime = Date.now();
const duration = ((endTime - startTime) / 1000).toFixed(2);
console.log(`\n✓ 首次扫描完成,耗时 ${duration} 秒`);
// 启动轮询
console.log(`\n⏰ 开始轮询,每 ${POLL_INTERVAL_MS / 1000} 秒检查一次新区块...\n`);
setInterval(async () => {
try {
// 如果正在扫描,跳过本次轮询
if (isScanning) {
console.log(`⏰ [${new Date().toLocaleString()}] 跳过本次轮询(上次扫描仍在进行中)`);
return;
}
const currentBlock = await provider.getBlockNumber();
// 如果有新区块,进行增量扫描
if (currentBlock > lastScannedBlock) {
isScanning = true; // 标记开始扫描
console.log(`\n${"=".repeat(60)}`);
console.log(`⏰ [${new Date().toLocaleString()}] 发现新区块`);
console.log(`${"=".repeat(60)}`);
const scanStart = Date.now();
await getAllHolders(provider, lastScannedBlock + 1, currentBlock, false);
const scanDuration = ((Date.now() - scanStart) / 1000).toFixed(2);
console.log(`\n✓ 增量扫描完成,耗时 ${scanDuration} 秒`);
isScanning = false; // 标记扫描完成
} else {
console.log(`⏰ [${new Date().toLocaleString()}] 暂无新区块 (当前: ${currentBlock})`);
}
} catch (error) {
console.error(`\n✗ 轮询过程中发生错误:`, error);
isScanning = false; // 发生错误时也要重置标记
}
}, POLL_INTERVAL_MS);
} catch (error) {
console.error("\n✗ 发生错误:", error);
process.exit(1);
}
}
main();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,477 +0,0 @@
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 的账户无金额限制地执行清算,确保及时清算水下账户,保护协议免受坏账风险。

View File

@@ -1,362 +0,0 @@
ytLending购买抵押品文档
目录
1.[快速开始](#快速开始)
2.[脚本功能说明](#脚本功能说明)
3.[环境变量配置](#环境变量配置)
4.[执行流程](#执行流程)
5.[常见问题](#常见问题)
快速开始
Bash
# 最小配置运行(自动扫描所有资产)
export LENDING_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"
npx hardhat run scripts/utils/buyCollateral.ts --network sepolia
# 指定滑点
export LENDING_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"
export SLIPPAGE="2"
npx hardhat run scripts/utils/buyCollateral.ts --network sepolia
脚本功能说明
什么是购买清算抵押品?
当借贷协议的储备金低于目标储备金时,协议会开放清算抵押品的购买功能。用户可以用折扣价格购买协议中的抵押品资产,帮助协议补充储备金。
主要功能
1. 自动扫描所有抵押品资产
脚本通过遍历合约的 assetList 数组,自动发现所有已配置的抵押品地址,无需手动指定。对每个资产检查其 collateralReserves跳过储备为零的资产。
2. 动态计算支付金额
无需预先指定 BASE_AMOUNT。每次购买前实时读取买家当前余额作为 baseAmount 上限传入合约。合约内部会将购买量自动限制到实际储备量,并通过 quoteBaseAmount() 只收取对应的实际费用,买家不会多付。
3. 基于储备量的滑点保护
Plain Text
minAmount = collateralReserves[asset] * (1 - slippage)
滑点作用于链上实际储备量,而非 quote 估算值,更准确地反映价格波动风险。默认滑点 1%,允许合理的价格偏移。
4. 多资产顺序购买,容错处理
•逐资产执行,单个资产失败不影响其他资产
•每次购买前重新读取买家余额,余额耗尽时提前退出并提示
•最终输出所有资产的购买汇总
5. 一次性 MaxUint256 授权
如果授权额度低于 MaxUint256 / 2自动执行一次 approve(MaxUint256),后续所有资产购买无需重复授权。
环境变量配置
必需参数
变量名 说明 示例
LENDING_ADDRESS Lending 合约地址 0x5FbDB2315678afecb367f032d93F642f64180aa3
可选参数
变量名 说明 默认值 取值范围
SLIPPAGE 滑点容忍度(百分比) 1 0-10推荐 1-2
> 说明: 原有的 `ASSET_ADDRESS` 和 `BASE_AMOUNT` 参数已移除。
•资产地址:由脚本自动从合约 assetList 扫描获取
•支付金额:由买家当前余额动态决定,合约只收取实际成本
配置示例
Bash
# 示例 1最小配置使用默认 1% 滑点
export LENDING_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"
# 示例 2指定 2% 滑点(高波动市场)
export LENDING_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"
export SLIPPAGE="2"
# 示例 3测试环境较高滑点
export LENDING_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"
export SLIPPAGE="5"
执行流程
Plain Text
启动脚本
└─ 读取环境变量,验证参数
1. 初始化
获取 Lending 合约、买家账户、baseToken 合约
2. 系统状态检查
检查 reserves < targetReserves
不满足则退出
3. 扫描可购买资产
遍历合约 assetList[]
过滤出 collateralReserves > 0 的资产
4. 授权检查
allowance < MaxUint256/2 ?
是 → approve(MaxUint256)
否 → 跳过
5. 逐资产购买循环
for each asset with reserves:
├─ 读取买家当前余额 buyerBalance
├─ 余额为 0 → break
├─ minAmount = reserves * (1 - slippage)
├─ buyCollateral(asset, minAmount, buyerBalance, buyer)
│ 合约内部:
│ collateralAmount = min(quoteCollateral(buyerBalance), reserves)
│ actualCost = quoteBaseAmount(collateralAmount)
│ transferFrom(buyer, actualCost) ← 只收实际费用
└─ 解析 BuyCollateral 事件,输出结果
6. 输出汇总
成功购买资产数 / 总数
累计支付 baseToken
买家剩余余额
### `getBuyCollateralInfo()` 辅助函数
该函数供外部调用,返回单个资产的购买预估信息:
返回字段 说明
availableReserve 链上可用抵押品储备量
expectedAmount 按 baseAmount 估算可购买数量(可能超过储备)
actualAmount 实际可购买数量(受储备限制后)
minAmount 应用滑点后的最小接受量
actualBaseAmount 估算实际需支付的 baseToken 数量
isLimited 是否受储备量限制
完整代码
TypeScript
import { ethers } from "hardhat";
import { Lending } from "../../typechain-types";
/**
* 购买清算抵押品脚本
*
* 自动扫描合约中所有抵押品资产,对有储备的资产执行购买。
* 传入买家当前余额作为 baseAmount 上限,合约自动按实际储备量收费。
* 无需指定具体资产地址,脚本会自动遍历合约的 assetList。
*
* 环境变量:
* - LENDING_ADDRESS: Lending 合约地址(必填)
* - SLIPPAGE (可选): 滑点容忍度百分比 (1-5),默认 1
*/
async function main() {
// ==================== 配置 ====================
const LENDING_ADDRESS = process.env.LENDING_ADDRESS;
const SLIPPAGE_PERCENT = parseFloat(process.env.SLIPPAGE || "1");
if (!LENDING_ADDRESS || LENDING_ADDRESS === "0x...") {
throw new Error("请设置 LENDING_ADDRESS 环境变量");
}
if (SLIPPAGE_PERCENT < 0 || SLIPPAGE_PERCENT > 10) {
throw new Error("SLIPPAGE 应在 0-10 之间");
}
const SLIPPAGE = SLIPPAGE_PERCENT / 100;
console.log("==================== 购买清算抵押品 ====================");
console.log(`Lending 合约: ${LENDING_ADDRESS}`);
console.log(`滑点容忍度: ${SLIPPAGE_PERCENT}%`);
console.log("");
// ==================== 初始化 ====================
const lending = await ethers.getContractAt("Lending", LENDING_ADDRESS) as unknown as Lending;
const [buyer] = await ethers.getSigners();
const baseToken = await lending.baseToken();
const base = await ethers.getContractAt("IERC20Metadata", baseToken);
const baseDecimals = await base.decimals();
console.log(`买家地址: ${buyer.address}`);
// ==================== 系统状态检查 ====================
console.log("\n检查系统状态...");
const reserves = await lending.getReserves();
const targetReserves = await lending.targetReserves();
console.log(`当前储备金: ${ethers.formatUnits(reserves, baseDecimals)} baseToken`);
console.log(`目标储备金: ${ethers.formatUnits(targetReserves, baseDecimals)} baseToken`);
if (reserves >= 0n && BigInt(reserves.toString()) >= targetReserves) {
throw new Error("储备金充足,当前无法购买抵押品");
}
// ==================== 扫描可购买资产 ====================
const assetsToProcess = await getAllAssets(lending);
console.log(`\n发现 ${assetsToProcess.length} 个抵押品资产`);
// 过滤出有储备的资产
const assetsWithReserves: { address: string; reserve: bigint; decimals: number }[] = [];
for (const assetAddr of assetsToProcess) {
const reserve = await lending.getCollateralReserves(assetAddr);
if (reserve > 0n) {
const assetToken = await ethers.getContractAt("IERC20Metadata", assetAddr);
const dec = await assetToken.decimals();
assetsWithReserves.push({ address: assetAddr, reserve, decimals: dec });
console.log(` ${assetAddr}: 储备 ${ethers.formatUnits(reserve, dec)} 代币`);
}
}
if (assetsWithReserves.length === 0) {
console.log("\n所有资产储备均为零无需购买。");
return;
}
// ==================== 授权(一次性 MaxUint256====================
console.log("\n检查授权...");
const allowance = await base.allowance(buyer.address, LENDING_ADDRESS);
if (allowance < ethers.MaxUint256 / 2n) {
console.log("正在授权 MaxUint256...");
const approveTx = await base.approve(LENDING_ADDRESS, ethers.MaxUint256);
await approveTx.wait();
console.log("授权成功");
} else {
console.log("授权充足,无需重复授权");
}
// ==================== 逐资产购买 ====================
let totalPaid = 0n;
let successCount = 0;
for (const { address: assetAddr, reserve, decimals: assetDecimals } of assetsWithReserves) {
console.log(`\n---- 购买资产: ${assetAddr} ----`);
// 读取买家当前余额作为本次最大支付额
const buyerBalance = await base.balanceOf(buyer.address);
if (buyerBalance === 0n) {
console.log("买家余额已耗尽,跳过剩余资产");
break;
}
console.log(`买家当前余额: ${ethers.formatUnits(buyerBalance, baseDecimals)} baseToken`);
console.log(`可用储备: ${ethers.formatUnits(reserve, assetDecimals)} 代币`);
// minAmount = 储备量 * (1 - slippage),允许价格轻微偏移
const slippageMultiplier = BigInt(Math.floor((1 - SLIPPAGE) * 1e18));
const minAmount = (reserve * slippageMultiplier) / BigInt(1e18);
console.log(`最小接受量 (${SLIPPAGE_PERCENT}% 滑点): ${ethers.formatUnits(minAmount, assetDecimals)} 代币`);
// 以买家全部余额作为 baseAmount 上限;合约内部按实际储备量收费
try {
const tx = await lending.buyCollateral(
assetAddr,
minAmount,
buyerBalance,
buyer.address
);
console.log(`交易已提交: ${tx.hash}`);
const receipt = await tx.wait();
console.log(`交易确认Gas 消耗: ${receipt?.gasUsed.toString()}`);
// 解析事件
const buyEvent = receipt?.logs.find((log: any) => {
try { return lending.interface.parseLog(log)?.name === "BuyCollateral"; }
catch { return false; }
});
if (buyEvent) {
const parsed = lending.interface.parseLog(buyEvent);
const paidAmount: bigint = parsed?.args.baseAmount;
const receivedAmount: bigint = parsed?.args.collateralAmount;
totalPaid += paidAmount;
successCount++;
console.log(`实际支付: ${ethers.formatUnits(paidAmount, baseDecimals)} baseToken`);
console.log(`实际获得: ${ethers.formatUnits(receivedAmount, assetDecimals)} 代币`);
// 折扣信息
const marketAmount = await lending.quoteCollateral(assetAddr, paidAmount);
if (receivedAmount > marketAmount && marketAmount > 0n) {
const discount = ((receivedAmount - marketAmount) * 10000n) / marketAmount;
console.log(`折扣收益: +${ethers.formatUnits(receivedAmount - marketAmount, assetDecimals)} 代币 (${Number(discount) / 100}%)`);
}
}
} catch (err: any) {
console.log(`跳过 ${assetAddr}${err.message?.split("\n")[0] ?? err}`);
}
}
// ==================== 汇总 ====================
console.log("\n==================== 购买汇总 ====================");
console.log(`成功购买资产数: ${successCount} / ${assetsWithReserves.length}`);
console.log(`累计支付: ${ethers.formatUnits(totalPaid, baseDecimals)} baseToken`);
const finalBalance = await base.balanceOf(buyer.address);
console.log(`买家剩余余额: ${ethers.formatUnits(finalBalance, baseDecimals)} baseToken`);
console.log("===================================================");
}
/**
* 遍历合约 assetList 数组,获取所有抵押品地址
*/
async function getAllAssets(lending: Lending): Promise<string[]> {
const assets: string[] = [];
let i = 0;
while (true) {
try {
const asset = await (lending as any).assetList(i);
assets.push(asset);
i++;
} catch {
break; // 数组越界,遍历结束
}
}
return assets;
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("\n执行失败:", error.message || error);
process.exit(1);
});
/**
* 获取单个资产的购买详情(供外部调用)
*/
export async function getBuyCollateralInfo(
lendingContract: Lending,
asset: string,
baseAmount: bigint,
slippageTolerance: number = 0.01
) {
const availableReserve = await lendingContract.getCollateralReserves(asset);
// minAmount 基于实际储备量而非 quote允许 slippage 偏移
const slippageMultiplier = BigInt(Math.floor((1 - slippageTolerance) * 1e18));
const minAmount = (availableReserve * slippageMultiplier) / BigInt(1e18);
// 用于展示:预估 baseAmount 能买到多少(可能超过储备,合约会自动限制)
const expectedAmount = await lendingContract.quoteCollateral(asset, baseAmount);
const actualAmount = expectedAmount < availableReserve ? expectedAmount : availableReserve;
const actualBaseAmount = actualAmount < expectedAmount
? (baseAmount * actualAmount) / expectedAmount
: baseAmount;
return {
availableReserve,
expectedAmount,
actualAmount,
minAmount,
baseAmount,
actualBaseAmount,
isLimited: actualAmount < expectedAmount,
};
}
常见问题
Q1: 什么时候可以购买抵押品?
当协议储备金低于目标储备金时才开放购买:
Plain Text
可购买条件reserves < targetReserves
可通过以下方式检查:
TypeScript
const reserves = await lending.getReserves();
const targetReserves = await lending.targetReserves();
console.log(`可以购买: ${reserves < targetReserves}`);
Q2: 滑点应该设置多少?
市场状况 建议滑点
正常市场 1默认
轻微波动 2
高波动市场 3-5
测试环境 5 或更高
滑点保护的含义:如果实际到手的抵押品数量低于 minAmount储备量的 1 - slippage 倍),交易会 revert。
Q3: 如果某个资产购买失败会怎样?
单个资产失败(如该资产储备金刚好被其他人买走、价格剧烈波动超出滑点)不会中断整个脚本,会打印错误信息后继续处理下一个资产。
Q4: 购买的折扣是如何计算的?
折扣通过清算因子liquidationFactor和价格因子storeFrontPriceFactor计算
Plain Text
discountFactor = storeFrontPriceFactor × liquidationFactor
effectiveAssetPrice = assetPrice × (1 - discountFactor)
示例:
Plain Text
assetPrice = $2.00
liquidationFactor = 0.05 (5%)
storeFrontPriceFactor = 0.90 (10%)
discountFactor = 0.90 × 0.05 = 0.045
effectivePrice = $2.00 × (1 - 0.045) ≈ $1.91 (折扣约 4.5%)
脚本会在每笔交易完成后自动计算实际折扣并输出。

File diff suppressed because it is too large Load Diff

View File

@@ -1,230 +0,0 @@
ytLp用户前端交互文档
1.用户添加流动性
Solidity
function addLiquidity(
address _token, //YT代币或WUSD地址
uint256 _amount, //代币数量
uint256 _minUsdy, //最小USDY数量
uint256 _minYtLP //最小ytLP数量
)
_minUsdy和_minYtLP计算方式滑点可以让用户在界面选择
TypeScript
/**
* 计算添加流动性的 _minUsdy 和 _minYtLP 参数
* @param {string} token - 代币地址
* @param {BigNumber} amount - 代币数量
* @param {number} slippageTolerance - 滑点容忍度 (0.005 = 0.5%)
* @returns {Promise<{minUsdy, minYtLP, expectedYtLP, feeInfo}>}
*/
async function calculateAddLiquidityParams(token, amount, slippageTolerance = 0.005) {
const PRICE_PRECISION = ethers.BigNumber.from("1000000000000000000000000000000");
const BASIS_POINTS = 10000;
try {
// 1. 获取价格使用MinPrice
const tokenPrice = await ytVault.getMinPrice(token);
// 2. 计算理论USDY扣费前
const theoreticalUsdy = amount.mul(tokenPrice).div(PRICE_PRECISION);
// 3. 获取手续费率
const feeBasisPoints = await ytVault.getSwapFeeBasisPoints(
token,
USDY_ADDRESS,
theoreticalUsdy
);
// 4. 计算扣费后的代币和USDY
const amountAfterFees = amount
.mul(BASIS_POINTS - feeBasisPoints)
.div(BASIS_POINTS);
const usdyAmount = amountAfterFees.mul(tokenPrice).div(PRICE_PRECISION);
// 5. 获取AUM和供应量
const [aum, ytLPSupply] = await Promise.all([
ytPoolManager.getAumInUsdy(true), // 使用MaxPrice
ytLP.totalSupply()
]);
// 6. 计算预期ytLP
let expectedYtLP;
if (ytLPSupply.eq(0)) {
expectedYtLP = usdyAmount;
} else {
expectedYtLP = usdyAmount.mul(ytLPSupply).div(aum);
}
// 7. 应用滑点
const minUsdy = usdyAmount.mul(
ethers.BigNumber.from(Math.floor((1 - slippageTolerance) * 10000))
).div(10000);
const minYtLP = expectedYtLP.mul(
ethers.BigNumber.from(Math.floor((1 - slippageTolerance) * 10000))
).div(10000);
return {
minUsdy,
minYtLP,
expectedYtLP,
usdyAmount,
feeInfo: {
feeBasisPoints: feeBasisPoints.toNumber(),
feeAmount: amount.sub(amountAfterFees),
amountAfterFees
},
priceInfo: {
tokenPrice: ethers.utils.formatUnits(tokenPrice, 30),
aum: ethers.utils.formatEther(aum),
ytLPSupply: ethers.utils.formatEther(ytLPSupply)
}
};
} catch (error) {
console.error("计算添加流动性参数失败:", error);
throw error;
}
}
2.用户移除流动性
Solidity
function removeLiquidity(
address _tokenOut, //出代币地址
uint256 _ytLPAmount, //ytLP数量
uint256 _minOut, //最小输出数量
address _receiver //接收地址
)
_minOut计算方式滑点可以让用户在界面选择
TypeScript
/**
* 计算移除流动性的 _minOut 参数
* @param {string} tokenOut - 目标代币地址
* @param {BigNumber} ytLPAmount - ytLP数量
* @param {number} slippageTolerance - 滑点容忍度 (0.01 = 1%)
* @returns {Promise<{minOut, expectedOut, feeBps, priceInfo}>}
*/
async function calculateMinOut(tokenOut, ytLPAmount, slippageTolerance = 0.01) {
const PRICE_PRECISION = ethers.BigNumber.from("1000000000000000000000000000000");
const BASIS_POINTS = 10000;
try {
// 1. 获取AUM和供应量
const [aum, ytLPSupply] = await Promise.all([
ytPoolManager.getAumInUsdy(false),
ytLP.totalSupply()
]);
// 2. 计算USDY价值
const usdyAmount = ytLPAmount.mul(aum).div(ytLPSupply);
// 3. 获取代币价格和手续费(并行查询)
const [tokenPrice, feeBasisPoints] = await Promise.all([
ytVault.getMaxPrice(tokenOut),
ytVault.getRedemptionFeeBasisPoints(tokenOut, usdyAmount)
]);
// 4. 计算理论输出
const theoreticalOut = usdyAmount.mul(PRICE_PRECISION).div(tokenPrice);
// 5. 扣除手续费
const expectedOut = theoreticalOut
.mul(BASIS_POINTS - feeBasisPoints)
.div(BASIS_POINTS);
// 6. 应用滑点
const minOut = expectedOut.mul(
ethers.BigNumber.from(Math.floor((1 - slippageTolerance) * 10000))
).div(10000);
return {
minOut,
expectedOut,
theoreticalOut,
usdyAmount,
feeBasisPoints: feeBasisPoints.toNumber(),
priceInfo: {
tokenPrice: ethers.utils.formatUnits(tokenPrice, 30),
aum: ethers.utils.formatEther(aum),
ytLPSupply: ethers.utils.formatEther(ytLPSupply)
}
};
} catch (error) {
console.error("计算_minOut失败:", error);
throw error;
}
}
3.用户交换代币
TypeScript
function swapYT(
address _tokenIn, //输入代币地址
address _tokenOut, //输出代币地址
uint256 _amountIn, //输入数量
uint256 _minOut, //最小输出数量
address _receiver //接收地址
)
_minOut计算方式滑点可以让用户在界面选择
TypeScript
/**
* 计算 swapYT 的 _minOut 参数
* @param {string} tokenIn - 输入代币地址
* @param {string} tokenOut - 输出代币地址
* @param {BigNumber} amountIn - 输入数量
* @param {number} slippageTolerance - 滑点容忍度 (0.005 = 0.5%)
* @returns {Promise<{minOut, expectedOut, feeInfo, priceInfo}>}
*/
async function calculateSwapMinOut(tokenIn, tokenOut, amountIn, slippageTolerance = 0.005) {
const PRICE_PRECISION = ethers.BigNumber.from("1000000000000000000000000000000");
const BASIS_POINTS = 10000;
try {
// 1. 获取价格(并行查询)
const [priceIn, priceOut] = await Promise.all([
ytVault.getMinPrice(tokenIn), // 输入用MinPrice
ytVault.getMaxPrice(tokenOut) // 输出用MaxPrice
]);
// 2. 计算USDY价值
const usdyAmount = amountIn.mul(priceIn).div(PRICE_PRECISION);
// 3. 获取手续费率
const feeBasisPoints = await ytVault.getSwapFeeBasisPoints(
tokenIn,
tokenOut,
usdyAmount
);
// 4. 计算理论输出
const theoreticalOut = usdyAmount.mul(PRICE_PRECISION).div(priceOut);
// 5. 扣除手续费
const expectedOut = theoreticalOut
.mul(BASIS_POINTS - feeBasisPoints)
.div(BASIS_POINTS);
// 6. 应用滑点
const minOut = expectedOut.mul(
ethers.BigNumber.from(Math.floor((1 - slippageTolerance) * 10000))
).div(10000);
return {
minOut,
expectedOut,
theoreticalOut,
usdyAmount,
feeBasisPoints: feeBasisPoints.toNumber(),
priceInfo: {
priceIn: ethers.utils.formatUnits(priceIn, 30),
priceOut: ethers.utils.formatUnits(priceOut, 30),
effectiveRate: theoreticalOut.mul(10000).div(amountIn).toNumber() / 100
},
feeInfo: {
feeBps: feeBasisPoints.toNumber(),
feeAmount: theoreticalOut.sub(expectedOut)
}
};
} catch (error) {
console.error("计算 _minOut 失败:", error);
throw error;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +0,0 @@
# assetx 后台内容
都需要admin 身份:
后台:
每次请求需要带token参数然后把token参数去认证中心请求
```jsx
post/api/auth/validate-token
Shell Curl
curl https://auth.upay01.com/api/auth/validate-token \
--request POST \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
```jsx
curl https://auth.upay01.com/api/auth/validate-token \
-X POST \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2OTgzMGJhYzNiM2QzOTJkNjJlODU3NDMiLCJ1c2VybmFtZSI6ImdfU29maW8iLCJlbWFpbCI6ImF3ZWkwMTAzMjVAZ21haWwuY29tIiwicm9sZXMiOlsidXNlciJdLCJ0eXBlIjoiYWNjZXNzIiwiaXNzIjoibWF4aWZpZ2h0LWF1dGgiLCJzdWIiOiI2OTgzMGJhYzNiM2QzOTJkNjJlODU3NDMiLCJleHAiOjE3NzEyOTg1NTEsIm5iZiI6MTc3MDY5Mzc1MSwiaWF0IjoxNzcwNjkzNzUxLCJqdGkiOiI2N2Q2ZjM2Mi01NzE2LTQ3OWYtODc3OS02OGE3NTVhNDBjMmMifQ.5sUndYufoz816YHz4oSJT1yvVNdiycS32yMGCVMFF-s"
```
```jsx
{"success":true,"message":"Token is valid","data":{"email":"awei010325@gmail.com","roles":["user"],"status":"active","tokenInfo":{"exp":"2026-02-17T03:22:31Z","iat":"2026-02-10T03:22:31Z","jti":"67d6f362-5716-479f-8779-68a755a40c2c"},"userID":1000000,"username":"g_Sofio"}}% sofio@sofiodeMac-Studio ~ %
```
token在服务端缓存5-60分钟
### 合约
合约交互(迁移旧版本的钱包交互的合约)
### 数据
1, 资产信息
2、资产details
3、overview
4、APY数据是实时的通过合约去查询计算
5、APY快照合约交互每2小时或者1小时快照apy数据存数据库对应的合约地址→ APY数据参考怎么去查看holder
6、人工传参数
7、asset detail → custody,(人工传信息CRUD)
8、/product footer 跳转链接,
9、没有提到的查看旧项目管理
### 10、ALPLending listLiquidity Allocation是自己维护
11、有k线数据的地方也是快照
12、transparency先不做
13、pointsholders每个小时快照→ holder持有A合约控制积分倍率。YT、LP、Lending。holders的积分数据根据现在的快照到数据库
1
### 邀请
1、web2的。生成连接钱包、请求签名类似于身份认证的token确认身份sign以后拿到hash值有expired7d存用户localstore→ 自动获取自己的邀请码
2、绑定前置也需要需要拿sign去绑定如果没有sign需要先sign
3、升级先不管
4、overview总积分RANK前10排名
5、我的邀请直推
6、每个操作对应一个积分规则倍率规则。规则的信息