2026-01-08 11:30:31 +08:00
|
|
|
|
import { ethers } from "hardhat";
|
|
|
|
|
|
import { Lending } from "../../typechain-types";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 购买清算抵押品脚本
|
2026-03-05 15:32:40 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 自动扫描合约中所有抵押品资产,对有储备的资产执行购买。
|
|
|
|
|
|
* 传入买家当前余额作为 baseAmount 上限,合约自动按实际储备量收费。
|
|
|
|
|
|
* 无需指定具体资产地址,脚本会自动遍历合约的 assetList。
|
|
|
|
|
|
*
|
2026-01-08 11:30:31 +08:00
|
|
|
|
* 环境变量:
|
2026-03-05 15:32:40 +08:00
|
|
|
|
* - LENDING_ADDRESS: Lending 合约地址(必填)
|
|
|
|
|
|
* - SLIPPAGE (可选): 滑点容忍度百分比 (1-5),默认 1
|
2026-01-08 11:30:31 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async function main() {
|
|
|
|
|
|
// ==================== 配置 ====================
|
|
|
|
|
|
const LENDING_ADDRESS = process.env.LENDING_ADDRESS;
|
2026-03-05 15:32:40 +08:00
|
|
|
|
const SLIPPAGE_PERCENT = parseFloat(process.env.SLIPPAGE || "1");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
|
|
|
|
|
|
if (!LENDING_ADDRESS || LENDING_ADDRESS === "0x...") {
|
2026-03-05 15:32:40 +08:00
|
|
|
|
throw new Error("请设置 LENDING_ADDRESS 环境变量");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
2026-03-05 15:32:40 +08:00
|
|
|
|
if (SLIPPAGE_PERCENT < 0 || SLIPPAGE_PERCENT > 10) {
|
|
|
|
|
|
throw new Error("SLIPPAGE 应在 0-10 之间");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
const SLIPPAGE = SLIPPAGE_PERCENT / 100;
|
2026-01-08 11:30:31 +08:00
|
|
|
|
|
|
|
|
|
|
console.log("==================== 购买清算抵押品 ====================");
|
|
|
|
|
|
console.log(`Lending 合约: ${LENDING_ADDRESS}`);
|
2026-03-05 15:32:40 +08:00
|
|
|
|
console.log(`滑点容忍度: ${SLIPPAGE_PERCENT}%`);
|
|
|
|
|
|
console.log("");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 初始化 ====================
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
console.log(`买家地址: ${buyer.address}`);
|
2026-01-08 11:30:31 +08:00
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// ==================== 系统状态检查 ====================
|
|
|
|
|
|
console.log("\n检查系统状态...");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
const reserves = await lending.getReserves();
|
|
|
|
|
|
const targetReserves = await lending.targetReserves();
|
2026-03-05 15:32:40 +08:00
|
|
|
|
console.log(`当前储备金: ${ethers.formatUnits(reserves, baseDecimals)} baseToken`);
|
|
|
|
|
|
console.log(`目标储备金: ${ethers.formatUnits(targetReserves, baseDecimals)} baseToken`);
|
2026-01-08 11:30:31 +08:00
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
if (reserves >= 0n && BigInt(reserves.toString()) >= targetReserves) {
|
|
|
|
|
|
throw new Error("储备金充足,当前无法购买抵押品");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// ==================== 扫描可购买资产 ====================
|
|
|
|
|
|
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)} 代币`);
|
|
|
|
|
|
}
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
if (assetsWithReserves.length === 0) {
|
|
|
|
|
|
console.log("\n所有资产储备均为零,无需购买。");
|
|
|
|
|
|
return;
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// ==================== 授权(一次性 MaxUint256)====================
|
|
|
|
|
|
console.log("\n检查授权...");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
const allowance = await base.allowance(buyer.address, LENDING_ADDRESS);
|
2026-03-05 15:32:40 +08:00
|
|
|
|
if (allowance < ethers.MaxUint256 / 2n) {
|
|
|
|
|
|
console.log("正在授权 MaxUint256...");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
const approveTx = await base.approve(LENDING_ADDRESS, ethers.MaxUint256);
|
|
|
|
|
|
await approveTx.wait();
|
2026-03-05 15:32:40 +08:00
|
|
|
|
console.log("授权成功");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
} else {
|
2026-03-05 15:32:40 +08:00
|
|
|
|
console.log("授权充足,无需重复授权");
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// ==================== 逐资产购买 ====================
|
|
|
|
|
|
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;
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
2026-03-05 15:32:40 +08:00
|
|
|
|
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)} 代币`);
|
2026-01-08 11:30:31 +08:00
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// 以买家全部余额作为 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}`);
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// ==================== 汇总 ====================
|
|
|
|
|
|
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;
|
2026-01-08 11:30:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main()
|
|
|
|
|
|
.then(() => process.exit(0))
|
|
|
|
|
|
.catch((error) => {
|
2026-03-05 15:32:40 +08:00
|
|
|
|
console.error("\n执行失败:", error.message || error);
|
2026-01-08 11:30:31 +08:00
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取单个资产的购买详情(供外部调用)
|
2026-01-08 11:30:31 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export async function getBuyCollateralInfo(
|
|
|
|
|
|
lendingContract: Lending,
|
|
|
|
|
|
asset: string,
|
|
|
|
|
|
baseAmount: bigint,
|
|
|
|
|
|
slippageTolerance: number = 0.01
|
|
|
|
|
|
) {
|
|
|
|
|
|
const availableReserve = await lendingContract.getCollateralReserves(asset);
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// minAmount 基于实际储备量而非 quote,允许 slippage 偏移
|
2026-01-08 11:30:31 +08:00
|
|
|
|
const slippageMultiplier = BigInt(Math.floor((1 - slippageTolerance) * 1e18));
|
2026-03-05 15:32:40 +08:00
|
|
|
|
const minAmount = (availableReserve * slippageMultiplier) / BigInt(1e18);
|
2026-01-08 11:30:31 +08:00
|
|
|
|
|
2026-03-05 15:32:40 +08:00
|
|
|
|
// 用于展示:预估 baseAmount 能买到多少(可能超过储备,合约会自动限制)
|
|
|
|
|
|
const expectedAmount = await lendingContract.quoteCollateral(asset, baseAmount);
|
|
|
|
|
|
const actualAmount = expectedAmount < availableReserve ? expectedAmount : availableReserve;
|
2026-01-08 11:30:31 +08:00
|
|
|
|
const actualBaseAmount = actualAmount < expectedAmount
|
2026-03-05 15:32:40 +08:00
|
|
|
|
? (baseAmount * actualAmount) / expectedAmount
|
2026-01-08 11:30:31 +08:00
|
|
|
|
: baseAmount;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-03-05 15:32:40 +08:00
|
|
|
|
availableReserve,
|
|
|
|
|
|
expectedAmount,
|
|
|
|
|
|
actualAmount,
|
|
|
|
|
|
minAmount,
|
|
|
|
|
|
baseAmount,
|
|
|
|
|
|
actualBaseAmount,
|
|
|
|
|
|
isLimited: actualAmount < expectedAmount,
|
2026-01-08 11:30:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|