chore: 移除根目录合约/设计文档,不纳入版本管理
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
528
holder获取方案文档.txt
528
holder获取方案文档.txt
@@ -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();
|
|
||||||
8729
usdc版本合约文档.txt
8729
usdc版本合约文档.txt
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 = 5000ms(5 秒)
|
|
||||||
循环步骤:
|
|
||||||
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] = 0,userCollateral[absorber][asset] += collateralAmount
|
|
||||||
6.清零债务:userBasic[Alice].principal = 0,totalBorrowBase -= 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 的账户无金额限制地执行清算,确保及时清算水下账户,保护协议免受坏账风险。
|
|
||||||
@@ -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
230
ytLp用户前端交互文档.txt
230
ytLp用户前端交互文档.txt
@@ -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
77
设计大概要求.txt
77
设计大概要求.txt
@@ -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、ALP,Lending list,Liquidity Allocation是自己维护
|
|
||||||
|
|
||||||
11、有k线数据的地方也是快照
|
|
||||||
|
|
||||||
12、transparency先不做
|
|
||||||
|
|
||||||
13、points,(holders每个小时快照)→ holder持有A合约,控制积分倍率。YT、LP、Lending。holders的积分数据,根据现在的快照到数据库,
|
|
||||||
|
|
||||||
1
|
|
||||||
|
|
||||||
### 邀请
|
|
||||||
|
|
||||||
1、web2的。生成:连接钱包、请求签名(类似于身份认证的token,确认身份,sign以后拿到hash值,有expired,7d,存用户localstore)→ 自动获取自己的邀请码
|
|
||||||
2、绑定:前置也需要需要拿sign去绑定,如果没有sign,需要先sign
|
|
||||||
|
|
||||||
3、升级:先不管
|
|
||||||
|
|
||||||
4、overview:总积分,RANK,前10排名
|
|
||||||
|
|
||||||
5、我的邀请:直推
|
|
||||||
|
|
||||||
6、每个操作对应一个积分规则,倍率规则。规则的信息,
|
|
||||||
Reference in New Issue
Block a user