update lending buyCollateral function

This commit is contained in:
2026-01-07 10:49:49 +08:00
parent 5f2750c80e
commit c8cb4dbecd
12 changed files with 468 additions and 30 deletions

View File

@@ -683,16 +683,198 @@ contract YtLendingTest is Test {
assertTrue(true, "Test completed");
}
function test_20_BuyCollateral_AutoCapToReserve() public {
// 0. Alice 先存入流动性
vm.prank(alice);
lending.supply(50000e6);
// 1. Bob 建立借款头寸并被清算
vm.startPrank(bob);
lending.supplyCollateral(address(ytVault), 10e18); // 10 YTToken @ $2000 = $20,000
lending.withdraw(16000e6);
vm.stopPrank();
ytFactory.updateVaultPrices(address(ytVault), 1750e30); // 价格跌到 $1,750
vm.prank(liquidator);
lending.absorb(bob);
// 2. 验证有 10 YTToken 的储备
assertEq(lending.getCollateralReserves(address(ytVault)), 10e18, "Should have 10 YTToken in reserves");
// 3. 价格进一步暴跌(模拟价格波动)
ytFactory.updateVaultPrices(address(ytVault), 500e30); // 价格暴跌到 $500
// 4. 计算:按 $500 计算,支付 5000 USDC 理论上能买到更多
// discount = 0.5 * (1 - 0.95) = 0.025 (2.5%)
// 折扣价 = 500 * (1 - 0.025) = $487.5
// 5000 / 487.5 = 10.26 YTToken超过储备的 10个
uint256 baseAmount = 5000e6; // 愿意支付 $5,000
uint256 liquidatorBalanceBefore = usdc.balanceOf(liquidator);
// 5. 购买抵押品(应该自动限制到 10 YTToken
vm.prank(liquidator);
lending.buyCollateral(
address(ytVault),
9e18, // minAmount: 至少要买到 9 个(允许一些滑点)
baseAmount, // 愿意支付 5000 USDC
liquidator
);
// 6. 验证结果
assertEq(ytVault.balanceOf(liquidator), 10e18, "Should receive exactly 10 YTToken (all reserves)");
assertEq(lending.getCollateralReserves(address(ytVault)), 0, "Reserves should be emptied");
// 7. 关键验证:只扣除了购买 10 YTToken 所需的费用,而不是全部 5000 USDC
uint256 actualPaid = liquidatorBalanceBefore - usdc.balanceOf(liquidator);
uint256 expectedPrice = 10e18 * 487.5e6 / 1e18; // 10 YTToken * $487.5 = $4,875
assertApproxEqAbs(actualPaid, expectedPrice, 1e6, "Should only pay for 10 YTToken, not the full baseAmount");
assertTrue(actualPaid < baseAmount, "Should pay less than the offered baseAmount");
}
function test_21_BuyCollateral_SlippageProtectionWithCap() public {
// 测试:当购买量被限制后,仍然要满足 minAmount 要求
// 设置场景:只有 5 YTToken 储备
vm.prank(alice);
lending.supply(50000e6);
vm.startPrank(charlie);
lending.supplyCollateral(address(ytVault), 5e18); // 只有 5 YTToken
lending.withdraw(8000e6);
vm.stopPrank();
ytFactory.updateVaultPrices(address(ytVault), 1750e30);
vm.prank(liquidator);
lending.absorb(charlie);
// 验证储备只有 5 YTToken
assertEq(lending.getCollateralReserves(address(ytVault)), 5e18, "Should have 5 YTToken in reserves");
// 价格暴跌,理论上能买到 20 个,但只有 5 个储备
ytFactory.updateVaultPrices(address(ytVault), 200e30);
// 尝试购买,但 minAmount 设置为 10储备只有 5
vm.prank(liquidator);
vm.expectRevert(ILending.InsufficientBalance.selector);
lending.buyCollateral(
address(ytVault),
10e18, // minAmount: 要求至少 10 个
10000e6, // 愿意支付很多
liquidator
);
// 但如果 minAmount 设置合理5个应该成功
vm.prank(liquidator);
lending.buyCollateral(
address(ytVault),
5e18, // minAmount: 5 个就可以
10000e6,
liquidator
);
assertEq(ytVault.balanceOf(liquidator), 5e18, "Should receive 5 YTToken");
}
function test_22_BuyCollateral_PriceIncreaseScenario() public {
// 测试价格上涨时购买量减少minAmount 提供保护
// 设置清算储备
vm.prank(alice);
lending.supply(50000e6);
vm.startPrank(bob);
lending.supplyCollateral(address(ytVault), 10e18);
lending.withdraw(16000e6);
vm.stopPrank();
ytFactory.updateVaultPrices(address(ytVault), 1750e30);
vm.prank(liquidator);
lending.absorb(bob);
// 价格回升(对购买者不利)
ytFactory.updateVaultPrices(address(ytVault), 3000e30); // 涨到 $3,000
// discount = 2.5%,折扣价 = 3000 * 0.975 = $2,925
// 支付 10000 USDC只能买到 10000 / 2925 ≈ 3.42 YTToken
uint256 baseAmount = 10000e6;
// 如果 minAmount 太高,应该失败(滑点保护)
vm.prank(liquidator);
vm.expectRevert(ILending.InsufficientBalance.selector);
lending.buyCollateral(
address(ytVault),
5e18, // 期望至少 5 个,但只能买到 3.42 个
baseAmount,
liquidator
);
// minAmount 设置合理则成功
vm.prank(liquidator);
lending.buyCollateral(
address(ytVault),
3e18, // 期望至少 3 个
baseAmount,
liquidator
);
// 验证大约买到 3.42 YTToken
assertApproxEqAbs(ytVault.balanceOf(liquidator), 3.42e18, 0.1e18, "Should receive ~3.42 YTToken");
}
function test_23_BuyCollateral_ExactReserveAmount() public {
// 测试:购买量刚好等于储备量的边界情况
vm.prank(alice);
lending.supply(50000e6);
vm.startPrank(bob);
lending.supplyCollateral(address(ytVault), 10e18);
lending.withdraw(16000e6);
vm.stopPrank();
ytFactory.updateVaultPrices(address(ytVault), 1750e30);
vm.prank(liquidator);
lending.absorb(bob);
// 计算购买 10 YTToken 需要的精确金额
// 价格 $1,750折扣 2.5%,折扣价 = $1,706.25
// 10 YTToken 需要 10 * 1706.25 = $17,062.50
uint256 exactAmount = 17062500000; // $17,062.50 (6 decimals)
uint256 quote = lending.quoteCollateral(address(ytVault), exactAmount);
assertEq(quote, 10e18, "Quote should be exactly 10 YTToken");
// 购买
uint256 liquidatorBalanceBefore = usdc.balanceOf(liquidator);
vm.prank(liquidator);
lending.buyCollateral(
address(ytVault),
10e18,
exactAmount,
liquidator
);
// 验证
assertEq(ytVault.balanceOf(liquidator), 10e18, "Should receive exactly 10 YTToken");
assertEq(lending.getCollateralReserves(address(ytVault)), 0, "Reserves should be zero");
// 验证支付了正确的金额
uint256 actualPaid = liquidatorBalanceBefore - usdc.balanceOf(liquidator);
assertApproxEqAbs(actualPaid, exactAmount, 1e6, "Should pay the exact quoted amount");
}
/*//////////////////////////////////////////////////////////////
RESERVES 测试
//////////////////////////////////////////////////////////////*/
function test_20_GetReserves_Initial() public view {
function test_24_GetReserves_Initial() public view {
// 初始储备金应该是 0
assertEq(lending.getReserves(), 0, "Initial reserves should be 0");
}
function test_21_GetReserves_AfterSupplyBorrow() public {
function test_25_GetReserves_AfterSupplyBorrow() public {
// Alice 存入 10,000 USDC
vm.prank(alice);
lending.supply(10000e6);
@@ -711,7 +893,7 @@ contract YtLendingTest is Test {
assertEq(lending.getReserves(), 0, "Reserves should still be 0");
}
function test_22_GetReserves_WithInterest() public {
function test_26_GetReserves_WithInterest() public {
// 建立借贷
vm.prank(alice);
lending.supply(10000e6);
@@ -738,7 +920,7 @@ contract YtLendingTest is Test {
assertApproxEqRel(uint256(reserves), 200e6, 0.005e18, "Reserves should be 200 USDC (0.5% tolerance)");
}
function test_23_WithdrawReserves_Success() public {
function test_27_WithdrawReserves_Success() public {
// 1. 累积储备金
vm.prank(alice);
lending.supply(10000e6);
@@ -770,13 +952,13 @@ contract YtLendingTest is Test {
);
}
function test_24_WithdrawReserves_FailInsufficientReserves() public {
function test_28_WithdrawReserves_FailInsufficientReserves() public {
// 尝试提取不存在的储备金
vm.expectRevert(ILending.InsufficientReserves.selector);
lending.withdrawReserves(address(0x999), 1000e6);
}
function test_25_WithdrawReserves_FailNotOwner() public {
function test_29_WithdrawReserves_FailNotOwner() public {
// 非 owner 尝试提取
vm.prank(alice);
vm.expectRevert();
@@ -787,7 +969,7 @@ contract YtLendingTest is Test {
VIEW FUNCTIONS 测试
//////////////////////////////////////////////////////////////*/
function test_26_GetUtilization() public {
function test_30_GetUtilization() public {
// 初始利用率应该是 0
assertEq(lending.getUtilization(), 0, "Initial utilization should be 0");
@@ -805,7 +987,7 @@ contract YtLendingTest is Test {
assertEq(lending.getUtilization(), 0.8e18, "Utilization should be 80%");
}
function test_27_GetSupplyRate_BelowKink() public {
function test_31_GetSupplyRate_BelowKink() public {
// 利用率 50%,低于 kink80%
vm.prank(alice);
lending.supply(10000e6);
@@ -823,7 +1005,7 @@ contract YtLendingTest is Test {
assertApproxEqRel(supplyRate, 0.015e18, 0.0001e18, "Supply rate should be 1.5% APY (0.01% tolerance)");
}
function test_28_GetBorrowRate_AtKink() public {
function test_32_GetBorrowRate_AtKink() public {
// 利用率正好 80%
vm.prank(alice);
lending.supply(10000e6);
@@ -842,7 +1024,7 @@ contract YtLendingTest is Test {
assertApproxEqRel(borrowRate, 0.055e18, 0.0001e18, "Borrow rate should be 5.5% APY (0.01% tolerance)");
}
function test_29_QuoteCollateral() public view {
function test_33_QuoteCollateral() public view {
// YTToken 价格 $2000, liquidationFactor 0.95, storeFrontFactor 0.5
// discount = 0.5 * (1 - 0.95) = 0.025 (2.5%)
// 折扣价 = 2000 * (1 - 0.025) = $1,950
@@ -854,11 +1036,217 @@ contract YtLendingTest is Test {
assertEq(expectedYTToken, 10e18, "Should quote 10 YTToken for 19,500 USDC");
}
function test_33a_QuoteCollateral_Reversibility() public {
// 测试quoteCollateral 和 quoteBaseAmount 的可逆性
// 即quote -> baseAmount -> quote 应该得到相同的结果
// 设置清算储备以便调用内部函数
vm.prank(alice);
lending.supply(50000e6);
vm.startPrank(bob);
lending.supplyCollateral(address(ytVault), 10e18);
lending.withdraw(16000e6);
vm.stopPrank();
ytFactory.updateVaultPrices(address(ytVault), 1750e30);
vm.prank(liquidator);
lending.absorb(bob);
// 测试 1: 给定 baseAmount计算 collateralAmount再反向计算回 baseAmount
uint256 originalBaseAmount = 10000e6; // 10,000 USDC
uint256 collateralAmount = lending.quoteCollateral(address(ytVault), originalBaseAmount);
// 购买这些抵押品,验证实际支付金额
uint256 liquidatorBalanceBefore = usdc.balanceOf(liquidator);
vm.prank(liquidator);
lending.buyCollateral(
address(ytVault),
0, // minAmount = 0
originalBaseAmount,
liquidator
);
uint256 actualPaid = liquidatorBalanceBefore - usdc.balanceOf(liquidator);
// 实际支付应该接近原始的 baseAmount或者如果被 cap 了则更少)
assertTrue(actualPaid <= originalBaseAmount, "Should not pay more than offered");
// 如果购买量没有被 cap实际支付应该非常接近计算值
if (collateralAmount <= 10e18) { // 没有超过储备
assertApproxEqRel(actualPaid, originalBaseAmount, 0.001e18, "Should pay the calculated amount (0.1% tolerance)");
}
}
function test_33b_QuoteBaseAmount_Accuracy() public {
// 测试quoteBaseAmount 的计算准确性
// 通过实际购买来验证计算是否正确
// 设置清算储备
vm.prank(alice);
lending.supply(50000e6);
vm.startPrank(bob);
lending.supplyCollateral(address(ytVault), 10e18);
lending.withdraw(16000e6);
vm.stopPrank();
// 价格设置为 $1,500
ytFactory.updateVaultPrices(address(ytVault), 1500e30);
vm.prank(liquidator);
lending.absorb(bob);
// 测试不同的购买量
uint256[] memory testAmounts = new uint256[](5);
testAmounts[0] = 1e18; // 1 YTToken
testAmounts[1] = 2.5e18; // 2.5 YTToken
testAmounts[2] = 5e18; // 5 YTToken
testAmounts[3] = 7.5e18; // 7.5 YTToken
testAmounts[4] = 10e18; // 10 YTToken
for (uint i = 0; i < testAmounts.length; i++) {
uint256 collateralAmount = testAmounts[i];
// 计算理论价格
// YTToken 价格 = $1,500
// discount = 0.5 * (1 - 0.95) = 0.025 (2.5%)
// 折扣价 = 1500 * (1 - 0.025) = $1,462.5
uint256 expectedBaseAmount = collateralAmount * 14625e5 / 1e18; // $1,462.5 per YT
// 通过 quoteCollateral 反向验证
uint256 calculatedCollateral = lending.quoteCollateral(address(ytVault), expectedBaseAmount);
// 应该能得到相同数量的抵押品(允许小误差)
assertApproxEqRel(
calculatedCollateral,
collateralAmount,
0.001e18, // 0.1% tolerance
string(abi.encodePacked("Quote mismatch for ", vm.toString(collateralAmount / 1e18), " YTToken"))
);
}
}
function test_33c_QuoteBaseAmount_DifferentPrices() public {
// 测试:验证 quoteCollateral 和实际购买的一致性(不同价格)
// 给 alice 足够的 USDC
usdc.mint(alice, 100000e6);
vm.prank(alice);
lending.supply(100000e6);
// 创建清算储备
vm.startPrank(bob);
lending.supplyCollateral(address(ytVault), 10e18);
lending.withdraw(16000e6);
vm.stopPrank();
// 测试不同价格
uint256[] memory testPrices = new uint256[](3);
testPrices[0] = 1000e30; // $1,000
testPrices[1] = 1750e30; // $1,750
testPrices[2] = 3000e30; // $3,000
for (uint i = 0; i < testPrices.length; i++) {
// 设置价格并触发清算
ytFactory.updateVaultPrices(address(ytVault), testPrices[i]);
// 确保可以清算(降低到清算阈值以下)
if (i == 0) { // 第一次需要清算
ytFactory.updateVaultPrices(address(ytVault), 1880e30);
vm.prank(liquidator);
lending.absorb(bob);
}
// 如果有储备,测试购买
if (lending.getCollateralReserves(address(ytVault)) > 0) {
ytFactory.updateVaultPrices(address(ytVault), testPrices[i]);
uint256 testPayment = 5000e6; // 支付 $5,000
uint256 expectedAmount = lending.quoteCollateral(address(ytVault), testPayment);
uint256 liquidatorBalanceBefore = usdc.balanceOf(liquidator);
uint256 liquidatorYTBefore = ytVault.balanceOf(liquidator);
vm.prank(liquidator);
lending.buyCollateral(
address(ytVault),
0, // minAmount = 0
testPayment,
liquidator
);
uint256 actualReceived = ytVault.balanceOf(liquidator) - liquidatorYTBefore;
uint256 actualPaid = liquidatorBalanceBefore - usdc.balanceOf(liquidator);
// 验证:如果购买量没被 cap应该得到期望的数量
uint256 reserves = lending.getCollateralReserves(address(ytVault));
if (expectedAmount <= reserves + actualReceived) {
assertApproxEqRel(actualReceived, expectedAmount, 0.001e18, "Should receive expected amount");
}
// 验证:实际支付应该合理
assertTrue(actualPaid <= testPayment, "Should not pay more than offered");
}
// 跳出循环(已经测试过了)
break;
}
}
function test_33d_QuoteBaseAmount_EdgeCases() public {
// 测试边界情况
vm.prank(alice);
lending.supply(50000e6);
vm.startPrank(bob);
lending.supplyCollateral(address(ytVault), 10e18); // 10 YT @ $2000 = $20,000
lending.withdraw(16000e6); // 借 $16,000 (80% LTV)
vm.stopPrank();
// 价格下跌触发清算
// 需要跌到清算阈值以下16000 / (10 * 0.85) = $1882
ytFactory.updateVaultPrices(address(ytVault), 1880e30); // 跌到 $1,880
vm.prank(liquidator);
lending.absorb(bob);
// 测试 1: 购买极小数量0.001 YT
// YT 价格 = $1,880, discount = 2.5%, 折扣价 = $1,833
uint256 tinyAmount = 0.001e18;
uint256 quote1 = lending.quoteCollateral(address(ytVault), 1833e3); // ~$1.833 (0.001 * $1833)
assertApproxEqAbs(quote1, tinyAmount, 0.0001e18, "Should handle tiny amounts");
// 测试 2: 购买大数量(刚好 10 YT
// 10 YT * $1,833 = $18,330
uint256 fullAmount = 10e18;
uint256 quote2 = lending.quoteCollateral(address(ytVault), 18330e6); // $18,330
assertApproxEqAbs(quote2, fullAmount, 0.01e18, "Should handle full reserve amount");
// 测试 3: 购买超过储备的数量(应该被 cap
uint256 hugePayment = 100000e6; // $100,000
uint256 liquidatorBalanceBefore = usdc.balanceOf(liquidator);
vm.prank(liquidator);
lending.buyCollateral(
address(ytVault),
0,
hugePayment,
liquidator
);
// 应该只购买了 10 YT全部储备
assertEq(ytVault.balanceOf(liquidator), 10e18, "Should be capped to reserve amount");
// 应该只支付了 10 YT 的费用,不是全部 100,000
uint256 actualPaid = liquidatorBalanceBefore - usdc.balanceOf(liquidator);
assertTrue(actualPaid < hugePayment, "Should not pay the full huge amount");
assertApproxEqAbs(actualPaid, 18330e6, 10e6, "Should pay only for 10 YT (~$18,330)");
}
/*//////////////////////////////////////////////////////////////
EDGE CASES 测试
//////////////////////////////////////////////////////////////*/
function test_30_Borrow_MaxLTV() public {
function test_34_Borrow_MaxLTV() public {
// Bob 先存入流动性
vm.prank(bob);
lending.supply(50000e6);
@@ -875,7 +1263,7 @@ contract YtLendingTest is Test {
vm.stopPrank();
}
function test_31_Borrow_FailOverLTV() public {
function test_35_Borrow_FailOverLTV() public {
// Bob 先存入流动性
vm.prank(bob);
lending.supply(50000e6);
@@ -890,7 +1278,7 @@ contract YtLendingTest is Test {
vm.stopPrank();
}
function test_32_WithdrawCollateral_FailIfBorrowing() public {
function test_36_WithdrawCollateral_FailIfBorrowing() public {
// Bob 先存入流动性
vm.prank(bob);
lending.supply(50000e6);
@@ -906,7 +1294,7 @@ contract YtLendingTest is Test {
vm.stopPrank();
}
function test_33_SupplyCollateral_FailExceedCap() public {
function test_37_SupplyCollateral_FailExceedCap() public {
// 尝试超过供应上限100,000 YTToken
// 需要先买足够的 YT
usdc.mint(alice, 200000000e6);
@@ -920,7 +1308,7 @@ contract YtLendingTest is Test {
vm.stopPrank();
}
function test_34_ComplexScenario_MultipleUsers() public {
function test_38_ComplexScenario_MultipleUsers() public {
// 1. Alice 存款
vm.prank(alice);
lending.supply(50000e6);
@@ -979,6 +1367,13 @@ contract YtLendingTest is Test {
MOCK CONTRACTS
//////////////////////////////////////////////////////////////*/
// Test wrapper to expose internal functions
contract LendingTestWrapper is Lending {
function quoteBaseAmountPublic(address asset, uint256 collateralAmount) external view returns (uint256) {
return quoteBaseAmount(asset, collateralAmount);
}
}
// Mock ERC20 for testing
contract MockERC20 is ERC20 {
uint8 private _decimals;