From 586b04a37100c26bb62a030394ac5895e1652428 Mon Sep 17 00:00:00 2001 From: xiaoJ <1143020035@qq.com> Date: Thu, 5 Mar 2026 15:32:40 +0800 Subject: [PATCH] update buy collateral script --- scripts/utils/buyCollateral.ts | 277 +++++++++++++++++---------------- 1 file changed, 139 insertions(+), 138 deletions(-) diff --git a/scripts/utils/buyCollateral.ts b/scripts/utils/buyCollateral.ts index fa68835..1451f07 100644 --- a/scripts/utils/buyCollateral.ts +++ b/scripts/utils/buyCollateral.ts @@ -3,35 +3,33 @@ import { Lending } from "../../typechain-types"; /** * 购买清算抵押品脚本 - * + * + * 自动扫描合约中所有抵押品资产,对有储备的资产执行购买。 + * 传入买家当前余额作为 baseAmount 上限,合约自动按实际储备量收费。 + * 无需指定具体资产地址,脚本会自动遍历合约的 assetList。 + * * 环境变量: - * - LENDING_ADDRESS: Lending 合约地址 - * - ASSET_ADDRESS: 抵押品资产地址 - * - BASE_AMOUNT (可选): 愿意支付的最大金额,默认 100 - * - SLIPPAGE (可选): 滑点容忍度 (1-5),默认 2 + * - LENDING_ADDRESS: Lending 合约地址(必填) + * - SLIPPAGE (可选): 滑点容忍度百分比 (1-5),默认 1 */ async function main() { // ==================== 配置 ==================== const LENDING_ADDRESS = process.env.LENDING_ADDRESS; - const ASSET_ADDRESS = process.env.ASSET_ADDRESS; - const BASE_AMOUNT_INPUT = process.env.BASE_AMOUNT || "100"; - const SLIPPAGE_PERCENT = parseInt(process.env.SLIPPAGE || "2"); + const SLIPPAGE_PERCENT = parseFloat(process.env.SLIPPAGE || "1"); - // 参数验证 if (!LENDING_ADDRESS || LENDING_ADDRESS === "0x...") { - throw new Error("❌ 请设置 LENDING_ADDRESS 环境变量"); + throw new Error("请设置 LENDING_ADDRESS 环境变量"); } - if (!ASSET_ADDRESS || ASSET_ADDRESS === "0x...") { - throw new Error("❌ 请设置 ASSET_ADDRESS 环境变量"); + if (SLIPPAGE_PERCENT < 0 || SLIPPAGE_PERCENT > 10) { + throw new Error("SLIPPAGE 应在 0-10 之间"); } - const SLIPPAGE = SLIPPAGE_PERCENT / 100; // 转换为小数 + const SLIPPAGE = SLIPPAGE_PERCENT / 100; console.log("==================== 购买清算抵押品 ===================="); console.log(`Lending 合约: ${LENDING_ADDRESS}`); - console.log(`抵押品地址: ${ASSET_ADDRESS}`); - console.log(`最大支付金额: ${BASE_AMOUNT_INPUT} USDC`); - console.log(`滑点容忍度: ${SLIPPAGE_PERCENT}%\n`); + console.log(`滑点容忍度: ${SLIPPAGE_PERCENT}%`); + console.log(""); // ==================== 初始化 ==================== const lending = await ethers.getContractAt("Lending", LENDING_ADDRESS) as unknown as Lending; @@ -39,148 +37,151 @@ async function main() { const baseToken = await lending.baseToken(); const base = await ethers.getContractAt("IERC20Metadata", baseToken); const baseDecimals = await base.decimals(); - - const BASE_AMOUNT = ethers.parseUnits(BASE_AMOUNT_INPUT, baseDecimals); - console.log(`买家地址: ${buyer.address}\n`); + console.log(`买家地址: ${buyer.address}`); - // ==================== 前置检查 ==================== - console.log("📊 检查系统状态..."); - - // 1. 检查储备金状态 + // ==================== 系统状态检查 ==================== + console.log("\n检查系统状态..."); const reserves = await lending.getReserves(); const targetReserves = await lending.targetReserves(); - console.log(`✓ 当前储备金: ${ethers.formatUnits(reserves, baseDecimals)} USDC`); - console.log(`✓ 目标储备金: ${ethers.formatUnits(targetReserves, baseDecimals)} USDC`); + console.log(`当前储备金: ${ethers.formatUnits(reserves, baseDecimals)} baseToken`); + console.log(`目标储备金: ${ethers.formatUnits(targetReserves, baseDecimals)} baseToken`); - if (reserves >= targetReserves) { - throw new Error("❌ 储备金充足,当前无法购买抵押品"); + if (reserves >= 0n && BigInt(reserves.toString()) >= targetReserves) { + throw new Error("储备金充足,当前无法购买抵押品"); } - // 2. 检查抵押品储备 - const collateralReserve = await lending.getCollateralReserves(ASSET_ADDRESS); - if (collateralReserve === 0n) { - throw new Error("❌ 该抵押品储备为空,无法购买"); - } - const asset = await ethers.getContractAt("IERC20Metadata", ASSET_ADDRESS); - const assetDecimals = await asset.decimals(); - console.log(`✓ 抵押品储备: ${ethers.formatUnits(collateralReserve, assetDecimals)} 代币\n`); + // ==================== 扫描可购买资产 ==================== + const assetsToProcess = await getAllAssets(lending); + console.log(`\n发现 ${assetsToProcess.length} 个抵押品资产`); - // 3. 检查买家余额 - const buyerBalance = await base.balanceOf(buyer.address); - console.log(`💰 买家余额: ${ethers.formatUnits(buyerBalance, baseDecimals)} USDC`); - if (buyerBalance < BASE_AMOUNT) { - throw new Error(`❌ 余额不足。需要: ${ethers.formatUnits(BASE_AMOUNT, baseDecimals)} USDC,当前: ${ethers.formatUnits(buyerBalance, baseDecimals)} USDC`); + // 过滤出有储备的资产 + 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)} 代币`); + } } - // ==================== 获取购买信息 ==================== - console.log("\n📋 计算购买详情..."); - const info = await getBuyCollateralInfo(lending, ASSET_ADDRESS, BASE_AMOUNT, SLIPPAGE); - - 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`); + if (assetsWithReserves.length === 0) { + console.log("\n所有资产储备均为零,无需购买。"); + return; } - // ==================== 授权检查 ==================== - console.log("\n🔐 检查授权..."); + // ==================== 授权(一次性 MaxUint256)==================== + console.log("\n检查授权..."); const allowance = await base.allowance(buyer.address, LENDING_ADDRESS); - - if (allowance < BASE_AMOUNT) { - console.log(`当前授权: ${ethers.formatUnits(allowance, baseDecimals)} USDC (不足)`); - console.log("正在授权..."); + if (allowance < ethers.MaxUint256 / 2n) { + console.log("正在授权 MaxUint256..."); const approveTx = await base.approve(LENDING_ADDRESS, ethers.MaxUint256); await approveTx.wait(); - console.log("✅ 授权成功"); + console.log("授权成功"); } else { - console.log("✓ 授权充足"); + console.log("授权充足,无需重复授权"); } - // ==================== 执行购买 ==================== - console.log("\n💸 执行购买交易..."); - const tx = await lending.buyCollateral( - ASSET_ADDRESS, - info.minAmount, - BASE_AMOUNT, - buyer.address - ); + // ==================== 逐资产购买 ==================== + let totalPaid = 0n; + let successCount = 0; - console.log(`交易已提交: ${tx.hash}`); - console.log("等待确认..."); - - const receipt = await tx.wait(); - console.log(`✅ 交易确认! Gas 消耗: ${receipt?.gasUsed.toString()}\n`); + for (const { address: assetAddr, reserve, decimals: assetDecimals } of assetsWithReserves) { + console.log(`\n---- 购买资产: ${assetAddr} ----`); - // ==================== 解析结果 ==================== - const buyEvent = receipt?.logs.find((log: any) => { + // 读取买家当前余额作为本次最大支付额 + 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 { - return lending.interface.parseLog(log)?.name === "BuyCollateral"; - } catch { - return false; - } - }); + 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()}`); - if (buyEvent) { - const parsedEvent = lending.interface.parseLog(buyEvent); - const paidAmount = parsedEvent?.args.baseAmount; - const receivedAmount = parsedEvent?.args.collateralAmount; - - console.log("==================== 交易结果 ===================="); - console.log(`✓ 实际支付: ${ethers.formatUnits(paidAmount, baseDecimals)} USDC`); - console.log(`✓ 实际获得: ${ethers.formatUnits(receivedAmount, assetDecimals)} 代币`); - console.log(`✓ 购买者: ${parsedEvent?.args.buyer}`); - console.log(`✓ 接收地址: ${buyer.address}`); - - // 计算实际单价和折扣信息 - // 实际单价 = 支付金额 / 获得数量 - const actualPricePerToken = (paidAmount * ethers.parseUnits("1", assetDecimals)) / receivedAmount; - - // 计算正常市场价(使用 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 saved = actualPricePerToken * marketAmount / ethers.parseUnits("1", assetDecimals) - paidAmount; - - 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(`ℹ️ 这是清算抵押品的折扣购买,价格低于市场价`); + // 解析事件 + 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("==================================================="); } - // ==================== 更新后状态 ==================== - console.log("\n📊 购买后状态:"); - const newBalance = await base.balanceOf(buyer.address); - const newAssetBalance = await asset.balanceOf(buyer.address); - console.log(`买家 USDC 余额: ${ethers.formatUnits(newBalance, baseDecimals)} USDC`); - console.log(`买家抵押品余额: ${ethers.formatUnits(newAssetBalance, assetDecimals)} 代币`); - - console.log("\n✅ 购买完成!"); + // ==================== 汇总 ==================== + 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 { + 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); + console.error("\n执行失败:", error.message || error); process.exit(1); }); - /** - * 获取详细的购买信息 +/** + * 获取单个资产的购买详情(供外部调用) */ export async function getBuyCollateralInfo( lendingContract: Lending, @@ -188,25 +189,25 @@ export async function getBuyCollateralInfo( baseAmount: bigint, slippageTolerance: number = 0.01 ) { - const expectedAmount = await lendingContract.quoteCollateral(asset, baseAmount); const availableReserve = await lendingContract.getCollateralReserves(asset); - const actualAmount = expectedAmount < availableReserve ? expectedAmount : availableReserve; - + // minAmount 基于实际储备量而非 quote,允许 slippage 偏移 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 - ? (baseAmount * actualAmount) / expectedAmount // 按比例计算 + ? (baseAmount * actualAmount) / expectedAmount : baseAmount; return { - expectedAmount, // 理想情况下可购买的数量 - availableReserve, // 协议可用储备 - actualAmount, // 实际可购买的数量(限制后) - minAmount, // 应用滑点保护后的最小值 - baseAmount, // 用户愿意支付的最大金额 - actualBaseAmount, // 实际需要支付的金额(可能更少) - isLimited: actualAmount < expectedAmount, // 是否受储备限制 + availableReserve, + expectedAmount, + actualAmount, + minAmount, + baseAmount, + actualBaseAmount, + isLimited: actualAmount < expectedAmount, }; } \ No newline at end of file