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

363 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

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

ytLending购买抵押品文档
目录
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%)
脚本会在每笔交易完成后自动计算实际折扣并输出。