init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
362
ytLending购买抵押品文档-new.txt
Normal file
362
ytLending购买抵押品文档-new.txt
Normal file
@@ -0,0 +1,362 @@
|
||||
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%)
|
||||
脚本会在每笔交易完成后自动计算实际折扣并输出。
|
||||
Reference in New Issue
Block a user