update buy collateral script

This commit is contained in:
2026-03-05 15:32:40 +08:00
parent e05762aa46
commit 586b04a371

View File

@@ -4,34 +4,32 @@ import { Lending } from "../../typechain-types";
/** /**
* 购买清算抵押品脚本 * 购买清算抵押品脚本
* *
* 自动扫描合约中所有抵押品资产,对有储备的资产执行购买。
* 传入买家当前余额作为 baseAmount 上限,合约自动按实际储备量收费。
* 无需指定具体资产地址,脚本会自动遍历合约的 assetList。
*
* 环境变量: * 环境变量:
* - LENDING_ADDRESS: Lending 合约地址 * - LENDING_ADDRESS: Lending 合约地址(必填)
* - ASSET_ADDRESS: 抵押品资产地址 * - SLIPPAGE (可选): 滑点容忍度百分比 (1-5),默认 1
* - BASE_AMOUNT (可选): 愿意支付的最大金额,默认 100
* - SLIPPAGE (可选): 滑点容忍度 (1-5),默认 2
*/ */
async function main() { async function main() {
// ==================== 配置 ==================== // ==================== 配置 ====================
const LENDING_ADDRESS = process.env.LENDING_ADDRESS; const LENDING_ADDRESS = process.env.LENDING_ADDRESS;
const ASSET_ADDRESS = process.env.ASSET_ADDRESS; const SLIPPAGE_PERCENT = parseFloat(process.env.SLIPPAGE || "1");
const BASE_AMOUNT_INPUT = process.env.BASE_AMOUNT || "100";
const SLIPPAGE_PERCENT = parseInt(process.env.SLIPPAGE || "2");
// 参数验证
if (!LENDING_ADDRESS || LENDING_ADDRESS === "0x...") { if (!LENDING_ADDRESS || LENDING_ADDRESS === "0x...") {
throw new Error("请设置 LENDING_ADDRESS 环境变量"); throw new Error("请设置 LENDING_ADDRESS 环境变量");
} }
if (!ASSET_ADDRESS || ASSET_ADDRESS === "0x...") { if (SLIPPAGE_PERCENT < 0 || SLIPPAGE_PERCENT > 10) {
throw new Error("❌ 请设置 ASSET_ADDRESS 环境变量"); throw new Error("SLIPPAGE 应在 0-10 之间");
} }
const SLIPPAGE = SLIPPAGE_PERCENT / 100; // 转换为小数 const SLIPPAGE = SLIPPAGE_PERCENT / 100;
console.log("==================== 购买清算抵押品 ===================="); console.log("==================== 购买清算抵押品 ====================");
console.log(`Lending 合约: ${LENDING_ADDRESS}`); console.log(`Lending 合约: ${LENDING_ADDRESS}`);
console.log(`抵押品地址: ${ASSET_ADDRESS}`); console.log(`滑点容忍度: ${SLIPPAGE_PERCENT}%`);
console.log(`最大支付金额: ${BASE_AMOUNT_INPUT} USDC`); console.log("");
console.log(`滑点容忍度: ${SLIPPAGE_PERCENT}%\n`);
// ==================== 初始化 ==================== // ==================== 初始化 ====================
const lending = await ethers.getContractAt("Lending", LENDING_ADDRESS) as unknown as Lending; const lending = await ethers.getContractAt("Lending", LENDING_ADDRESS) as unknown as Lending;
@@ -40,147 +38,150 @@ async function main() {
const base = await ethers.getContractAt("IERC20Metadata", baseToken); const base = await ethers.getContractAt("IERC20Metadata", baseToken);
const baseDecimals = await base.decimals(); const baseDecimals = await base.decimals();
const BASE_AMOUNT = ethers.parseUnits(BASE_AMOUNT_INPUT, baseDecimals); console.log(`买家地址: ${buyer.address}`);
console.log(`买家地址: ${buyer.address}\n`); // ==================== 系统状态检查 ====================
console.log("\n检查系统状态...");
// ==================== 前置检查 ====================
console.log("📊 检查系统状态...");
// 1. 检查储备金状态
const reserves = await lending.getReserves(); const reserves = await lending.getReserves();
const targetReserves = await lending.targetReserves(); const targetReserves = await lending.targetReserves();
console.log(`当前储备金: ${ethers.formatUnits(reserves, baseDecimals)} USDC`); console.log(`当前储备金: ${ethers.formatUnits(reserves, baseDecimals)} baseToken`);
console.log(`目标储备金: ${ethers.formatUnits(targetReserves, baseDecimals)} USDC`); console.log(`目标储备金: ${ethers.formatUnits(targetReserves, baseDecimals)} baseToken`);
if (reserves >= targetReserves) { if (reserves >= 0n && BigInt(reserves.toString()) >= targetReserves) {
throw new Error("储备金充足,当前无法购买抵押品"); throw new Error("储备金充足,当前无法购买抵押品");
} }
// 2. 检查抵押品储备 // ==================== 扫描可购买资产 ====================
const collateralReserve = await lending.getCollateralReserves(ASSET_ADDRESS); const assetsToProcess = await getAllAssets(lending);
if (collateralReserve === 0n) { console.log(`\n发现 ${assetsToProcess.length} 个抵押品资产`);
throw new Error("❌ 该抵押品储备为空,无法购买");
}
const asset = await ethers.getContractAt("IERC20Metadata", ASSET_ADDRESS);
const assetDecimals = await asset.decimals();
console.log(`✓ 抵押品储备: ${ethers.formatUnits(collateralReserve, assetDecimals)} 代币\n`);
// 3. 检查买家余额 // 过滤出有储备的资产
const buyerBalance = await base.balanceOf(buyer.address); const assetsWithReserves: { address: string; reserve: bigint; decimals: number }[] = [];
console.log(`💰 买家余额: ${ethers.formatUnits(buyerBalance, baseDecimals)} USDC`); for (const assetAddr of assetsToProcess) {
if (buyerBalance < BASE_AMOUNT) { const reserve = await lending.getCollateralReserves(assetAddr);
throw new Error(`❌ 余额不足。需要: ${ethers.formatUnits(BASE_AMOUNT, baseDecimals)} USDC当前: ${ethers.formatUnits(buyerBalance, baseDecimals)} USDC`); 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📋 计算购买详情..."); console.log("\n所有资产储备均为零,无需购买。");
const info = await getBuyCollateralInfo(lending, ASSET_ADDRESS, BASE_AMOUNT, SLIPPAGE); return;
console.log(`✓ 预期购买: ${ethers.formatUnits(info.expectedAmount, assetDecimals)} 代币`);
console.log(`✓ 实际购买: ${ethers.formatUnits(info.actualAmount, assetDecimals)} 代币`);
console.log(`✓ 最小接受: ${ethers.formatUnits(info.minAmount, assetDecimals)} 代币`);
if (info.isLimited) {
console.log(`\n⚠ 储备不足,购买量已调整:`);
console.log(` 原计划支付: ${ethers.formatUnits(info.baseAmount, baseDecimals)} USDC`);
console.log(` 实际约支付: ${ethers.formatUnits(info.actualBaseAmount, baseDecimals)} USDC`);
console.log(` 节省约: ${ethers.formatUnits(info.baseAmount - info.actualBaseAmount, baseDecimals)} USDC`);
} }
// ==================== 授权检查 ==================== // ==================== 授权(一次性 MaxUint256====================
console.log("\n🔐 检查授权..."); console.log("\n检查授权...");
const allowance = await base.allowance(buyer.address, LENDING_ADDRESS); const allowance = await base.allowance(buyer.address, LENDING_ADDRESS);
if (allowance < ethers.MaxUint256 / 2n) {
if (allowance < BASE_AMOUNT) { console.log("正在授权 MaxUint256...");
console.log(`当前授权: ${ethers.formatUnits(allowance, baseDecimals)} USDC (不足)`);
console.log("正在授权...");
const approveTx = await base.approve(LENDING_ADDRESS, ethers.MaxUint256); const approveTx = await base.approve(LENDING_ADDRESS, ethers.MaxUint256);
await approveTx.wait(); await approveTx.wait();
console.log("授权成功"); console.log("授权成功");
} else { } else {
console.log("授权充足"); console.log("授权充足,无需重复授权");
} }
// ==================== 执行购买 ==================== // ==================== 逐资产购买 ====================
console.log("\n💸 执行购买交易..."); 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( const tx = await lending.buyCollateral(
ASSET_ADDRESS, assetAddr,
info.minAmount, minAmount,
BASE_AMOUNT, buyerBalance,
buyer.address buyer.address
); );
console.log(`交易已提交: ${tx.hash}`); console.log(`交易已提交: ${tx.hash}`);
console.log("等待确认...");
const receipt = await tx.wait(); const receipt = await tx.wait();
console.log(`交易确认! Gas 消耗: ${receipt?.gasUsed.toString()}\n`); console.log(`交易确认Gas 消耗: ${receipt?.gasUsed.toString()}`);
// ==================== 解析结果 ==================== // 解析事件
const buyEvent = receipt?.logs.find((log: any) => { const buyEvent = receipt?.logs.find((log: any) => {
try { try { return lending.interface.parseLog(log)?.name === "BuyCollateral"; }
return lending.interface.parseLog(log)?.name === "BuyCollateral"; catch { return false; }
} catch {
return false;
}
}); });
if (buyEvent) { if (buyEvent) {
const parsedEvent = lending.interface.parseLog(buyEvent); const parsed = lending.interface.parseLog(buyEvent);
const paidAmount = parsedEvent?.args.baseAmount; const paidAmount: bigint = parsed?.args.baseAmount;
const receivedAmount = parsedEvent?.args.collateralAmount; const receivedAmount: bigint = parsed?.args.collateralAmount;
totalPaid += paidAmount;
successCount++;
console.log("==================== 交易结果 ===================="); console.log(`实际支付: ${ethers.formatUnits(paidAmount, baseDecimals)} baseToken`);
console.log(`✓ 实际支付: ${ethers.formatUnits(paidAmount, baseDecimals)} USDC`); console.log(`实际获得: ${ethers.formatUnits(receivedAmount, assetDecimals)} 代币`);
console.log(`✓ 实际获得: ${ethers.formatUnits(receivedAmount, assetDecimals)} 代币`);
console.log(`✓ 购买者: ${parsedEvent?.args.buyer}`);
console.log(`✓ 接收地址: ${buyer.address}`);
// 计算实际单价和折扣信息 // 折扣信息
// 实际单价 = 支付金额 / 获得数量 const marketAmount = await lending.quoteCollateral(assetAddr, paidAmount);
const actualPricePerToken = (paidAmount * ethers.parseUnits("1", assetDecimals)) / receivedAmount; if (receivedAmount > marketAmount && marketAmount > 0n) {
// 计算正常市场价(使用 quoteCollateral 反推)
// 如果用相同的 baseAmount 在市场价购买,能买到多少代币
const marketAmount = await lending.quoteCollateral(ASSET_ADDRESS, paidAmount);
console.log(`\n💰 折扣购买说明:`);
console.log(`✓ 实际单价: ${ethers.formatUnits(actualPricePerToken, baseDecimals)} USDC/代币`);
// 只有当实际购买量大于市场价购买量时才显示折扣
if (receivedAmount > marketAmount) {
const discount = ((receivedAmount - marketAmount) * 10000n) / marketAmount; const discount = ((receivedAmount - marketAmount) * 10000n) / marketAmount;
const saved = actualPricePerToken * marketAmount / ethers.parseUnits("1", assetDecimals) - paidAmount; console.log(`折扣收益: +${ethers.formatUnits(receivedAmount - marketAmount, assetDecimals)} 代币 (${Number(discount) / 100}%)`);
console.log(`✓ 市场价可购买: ${ethers.formatUnits(marketAmount, assetDecimals)} 代币`);
console.log(`✓ 折扣多得: ${ethers.formatUnits(receivedAmount - marketAmount, assetDecimals)} 代币 (${Number(discount) / 100}%)`);
console.log(`✓ 相当于节省: ${ethers.formatUnits(saved, baseDecimals)} USDC`);
} else {
console.log(` 这是清算抵押品的折扣购买,价格低于市场价`);
} }
}
} 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("==================================================="); console.log("===================================================");
} }
// ==================== 更新后状态 ==================== /**
console.log("\n📊 购买后状态:"); * 遍历合约 assetList 数组,获取所有抵押品地址
const newBalance = await base.balanceOf(buyer.address); */
const newAssetBalance = await asset.balanceOf(buyer.address); async function getAllAssets(lending: Lending): Promise<string[]> {
console.log(`买家 USDC 余额: ${ethers.formatUnits(newBalance, baseDecimals)} USDC`); const assets: string[] = [];
console.log(`买家抵押品余额: ${ethers.formatUnits(newAssetBalance, assetDecimals)} 代币`); let i = 0;
while (true) {
console.log("\n✅ 购买完成!"); try {
const asset = await (lending as any).assetList(i);
assets.push(asset);
i++;
} catch {
break; // 数组越界,遍历结束
}
}
return assets;
} }
main() main()
.then(() => process.exit(0)) .then(() => process.exit(0))
.catch((error) => { .catch((error) => {
console.error("\n执行失败:", error.message || error); console.error("\n执行失败:", error.message || error);
process.exit(1); process.exit(1);
}); });
/** /**
* 获取详细的购买信息 * 获取单个资产的购买详情(供外部调用)
*/ */
export async function getBuyCollateralInfo( export async function getBuyCollateralInfo(
lendingContract: Lending, lendingContract: Lending,
@@ -188,25 +189,25 @@ export async function getBuyCollateralInfo(
baseAmount: bigint, baseAmount: bigint,
slippageTolerance: number = 0.01 slippageTolerance: number = 0.01
) { ) {
const expectedAmount = await lendingContract.quoteCollateral(asset, baseAmount);
const availableReserve = await lendingContract.getCollateralReserves(asset); const availableReserve = await lendingContract.getCollateralReserves(asset);
const actualAmount = expectedAmount < availableReserve ? expectedAmount : availableReserve; // minAmount 基于实际储备量而非 quote允许 slippage 偏移
const slippageMultiplier = BigInt(Math.floor((1 - slippageTolerance) * 1e18)); const slippageMultiplier = BigInt(Math.floor((1 - slippageTolerance) * 1e18));
const minAmount = (actualAmount * slippageMultiplier) / BigInt(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 const actualBaseAmount = actualAmount < expectedAmount
? (baseAmount * actualAmount) / expectedAmount // 按比例计算 ? (baseAmount * actualAmount) / expectedAmount
: baseAmount; : baseAmount;
return { return {
expectedAmount, // 理想情况下可购买的数量 availableReserve,
availableReserve, // 协议可用储备 expectedAmount,
actualAmount, // 实际可购买的数量(限制后) actualAmount,
minAmount, // 应用滑点保护后的最小值 minAmount,
baseAmount, // 用户愿意支付的最大金额 baseAmount,
actualBaseAmount, // 实际需要支付的金额(可能更少) actualBaseAmount,
isLimited: actualAmount < expectedAmount, // 是否受储备限制 isLimited: actualAmount < expectedAmount,
}; };
} }