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%)
脚本会在每笔交易完成后自动计算实际折扣并输出。
