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

File diff suppressed because one or more lines are too long

View File

@@ -373,6 +373,7 @@ contract Lending is
/**
* @notice 购买清算后的抵押品
* @dev 自动限制购买量到可用储备,只收取实际需要的费用
*/
function buyCollateral(
address asset,
@@ -388,15 +389,24 @@ contract Lending is
revert NotForSale(); // 储备金充足,无需出售
}
// 计算可购买的抵押品数量
// 计算可购买的抵押品数量(基于用户愿意支付的 baseAmount
uint256 collateralAmount = quoteCollateral(asset, baseAmount);
// 验证数
if (collateralAmount < minAmount) revert InsufficientBalance();
if (collateralAmount > collateralReserves[asset]) revert InsufficientBalance();
// 自动限制到可用储备
// 这样可以防止价格波动导致交易失败
if (collateralAmount > collateralReserves[asset]) {
collateralAmount = collateralReserves[asset];
}
// 收取清算人支付的资金
IERC20(baseToken).transferFrom(msg.sender, address(this), baseAmount);
// 滑点保护:确保购买量不低于用户的最小期望
if (collateralAmount < minAmount) revert InsufficientBalance();
// 根据实际购买量计算需要支付的金额(而非固定的 baseAmount
// 这样如果购买量被限制,用户只需支付相应的费用
uint256 actualBaseAmount = quoteBaseAmount(asset, collateralAmount);
// 收取实际需要的资金
IERC20(baseToken).transferFrom(msg.sender, address(this), actualBaseAmount);
// 抵押品出库
collateralReserves[asset] -= collateralAmount;
@@ -405,7 +415,38 @@ contract Lending is
IERC20(asset).safeTransfer(recipient, collateralAmount);
// 注意:收入会自动体现在 getReserves() 中,因为 balance 增加了
emit BuyCollateral(msg.sender, asset, baseAmount, collateralAmount);
emit BuyCollateral(msg.sender, asset, actualBaseAmount, collateralAmount);
}
/**
* @notice 计算购买指定数量抵押品需要支付的 baseToken 数量(反向计算)
* @param asset 抵押品地址
* @param collateralAmount 要购买的抵押品数量
* @return 需要支付的 baseToken 数量
*/
function quoteBaseAmount(address asset, uint256 collateralAmount) internal view returns (uint256) {
AssetConfig memory assetConfig = assetConfigs[asset];
uint256 assetPrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset);
uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken);
uint256 FACTOR_SCALE = 1e18;
uint256 baseScale = 10 ** uint256(IERC20Metadata(baseToken).decimals());
uint256 assetScale = 10 ** uint256(assetConfig.decimals);
// 计算折扣因子
uint256 discountFactor = (storeFrontPriceFactor * (FACTOR_SCALE - assetConfig.liquidationFactor)) / FACTOR_SCALE;
// 计算折扣后的资产价格
uint256 effectiveAssetPrice = (assetPrice * (FACTOR_SCALE - discountFactor)) / FACTOR_SCALE;
// 反向计算baseAmount = (collateralAmount * effectiveAssetPrice * baseScale) / (basePrice * assetScale)
if (baseScale == assetScale) {
return (collateralAmount * effectiveAssetPrice) / basePrice;
} else {
uint256 adjustedAmount = (collateralAmount * baseScale) / assetScale;
return (adjustedAmount * effectiveAssetPrice) / basePrice;
}
}
/**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"id":"be3a4ea4c5f927a0","source_id_to_path":{"0":"contracts/interfaces/ILending.sol","1":"contracts/interfaces/IYTAssetVault.sol","2":"contracts/interfaces/IYTLendingPriceFeed.sol","3":"contracts/ytLending/Configurator.sol","4":"contracts/ytLending/ConfiguratorStorage.sol","5":"contracts/ytLending/Lending.sol","6":"contracts/ytLending/LendingConfiguration.sol","7":"contracts/ytLending/LendingFactory.sol","8":"contracts/ytLending/LendingMath.sol","9":"contracts/ytLending/LendingPriceFeed.sol","10":"contracts/ytLending/LendingStorage.sol","11":"contracts/ytVault/YTAssetFactory.sol","12":"contracts/ytVault/YTAssetVault.sol","13":"lib/forge-std/src/Base.sol","14":"lib/forge-std/src/StdAssertions.sol","15":"lib/forge-std/src/StdChains.sol","16":"lib/forge-std/src/StdCheats.sol","17":"lib/forge-std/src/StdConstants.sol","18":"lib/forge-std/src/StdError.sol","19":"lib/forge-std/src/StdInvariant.sol","20":"lib/forge-std/src/StdJson.sol","21":"lib/forge-std/src/StdMath.sol","22":"lib/forge-std/src/StdStorage.sol","23":"lib/forge-std/src/StdStyle.sol","24":"lib/forge-std/src/StdToml.sol","25":"lib/forge-std/src/StdUtils.sol","26":"lib/forge-std/src/Test.sol","27":"lib/forge-std/src/Vm.sol","28":"lib/forge-std/src/console.sol","29":"lib/forge-std/src/console2.sol","30":"lib/forge-std/src/interfaces/IMulticall3.sol","31":"lib/forge-std/src/safeconsole.sol","32":"node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol","33":"node_modules/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol","34":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol","35":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol","36":"node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol","37":"node_modules/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol","38":"node_modules/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol","39":"node_modules/@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol","40":"node_modules/@openzeppelin/contracts/access/Ownable.sol","41":"node_modules/@openzeppelin/contracts/interfaces/IERC1363.sol","42":"node_modules/@openzeppelin/contracts/interfaces/IERC165.sol","43":"node_modules/@openzeppelin/contracts/interfaces/IERC1967.sol","44":"node_modules/@openzeppelin/contracts/interfaces/IERC20.sol","45":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC1822.sol","46":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC6093.sol","47":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol","48":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol","49":"node_modules/@openzeppelin/contracts/proxy/Proxy.sol","50":"node_modules/@openzeppelin/contracts/proxy/beacon/IBeacon.sol","51":"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol","52":"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol","53":"node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol","54":"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol","55":"node_modules/@openzeppelin/contracts/utils/Address.sol","56":"node_modules/@openzeppelin/contracts/utils/Context.sol","57":"node_modules/@openzeppelin/contracts/utils/Errors.sol","58":"node_modules/@openzeppelin/contracts/utils/StorageSlot.sol","59":"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol","60":"test/YtLending.t.sol"},"language":"Solidity"}
{"id":"97e3076db82493b4","source_id_to_path":{"0":"contracts/interfaces/ILending.sol","1":"contracts/interfaces/IYTAssetVault.sol","2":"contracts/interfaces/IYTLendingPriceFeed.sol","3":"contracts/ytLending/Configurator.sol","4":"contracts/ytLending/ConfiguratorStorage.sol","5":"contracts/ytLending/Lending.sol","6":"contracts/ytLending/LendingConfiguration.sol","7":"contracts/ytLending/LendingFactory.sol","8":"contracts/ytLending/LendingMath.sol","9":"contracts/ytLending/LendingPriceFeed.sol","10":"contracts/ytLending/LendingStorage.sol","11":"contracts/ytVault/YTAssetFactory.sol","12":"contracts/ytVault/YTAssetVault.sol","13":"lib/forge-std/src/Base.sol","14":"lib/forge-std/src/StdAssertions.sol","15":"lib/forge-std/src/StdChains.sol","16":"lib/forge-std/src/StdCheats.sol","17":"lib/forge-std/src/StdConstants.sol","18":"lib/forge-std/src/StdError.sol","19":"lib/forge-std/src/StdInvariant.sol","20":"lib/forge-std/src/StdJson.sol","21":"lib/forge-std/src/StdMath.sol","22":"lib/forge-std/src/StdStorage.sol","23":"lib/forge-std/src/StdStyle.sol","24":"lib/forge-std/src/StdToml.sol","25":"lib/forge-std/src/StdUtils.sol","26":"lib/forge-std/src/Test.sol","27":"lib/forge-std/src/Vm.sol","28":"lib/forge-std/src/console.sol","29":"lib/forge-std/src/console2.sol","30":"lib/forge-std/src/interfaces/IMulticall3.sol","31":"lib/forge-std/src/safeconsole.sol","32":"node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol","33":"node_modules/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol","34":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol","35":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol","36":"node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol","37":"node_modules/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol","38":"node_modules/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol","39":"node_modules/@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol","40":"node_modules/@openzeppelin/contracts/access/Ownable.sol","41":"node_modules/@openzeppelin/contracts/interfaces/IERC1363.sol","42":"node_modules/@openzeppelin/contracts/interfaces/IERC165.sol","43":"node_modules/@openzeppelin/contracts/interfaces/IERC1967.sol","44":"node_modules/@openzeppelin/contracts/interfaces/IERC20.sol","45":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC1822.sol","46":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC6093.sol","47":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol","48":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol","49":"node_modules/@openzeppelin/contracts/proxy/Proxy.sol","50":"node_modules/@openzeppelin/contracts/proxy/beacon/IBeacon.sol","51":"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol","52":"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol","53":"node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol","54":"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol","55":"node_modules/@openzeppelin/contracts/utils/Address.sol","56":"node_modules/@openzeppelin/contracts/utils/Context.sol","57":"node_modules/@openzeppelin/contracts/utils/Errors.sol","58":"node_modules/@openzeppelin/contracts/utils/StorageSlot.sol","59":"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol","60":"test/YtLending.t.sol"},"language":"Solidity"}

View File

@@ -0,0 +1 @@
{"id":"c25683059ee87c23","source_id_to_path":{"0":"contracts/interfaces/ILending.sol","1":"contracts/interfaces/IYTAssetVault.sol","2":"contracts/interfaces/IYTLendingPriceFeed.sol","3":"contracts/ytLending/Configurator.sol","4":"contracts/ytLending/ConfiguratorStorage.sol","5":"contracts/ytLending/Lending.sol","6":"contracts/ytLending/LendingConfiguration.sol","7":"contracts/ytLending/LendingFactory.sol","8":"contracts/ytLending/LendingMath.sol","9":"contracts/ytLending/LendingPriceFeed.sol","10":"contracts/ytLending/LendingStorage.sol","11":"contracts/ytVault/YTAssetFactory.sol","12":"contracts/ytVault/YTAssetVault.sol","13":"lib/forge-std/src/Base.sol","14":"lib/forge-std/src/StdAssertions.sol","15":"lib/forge-std/src/StdChains.sol","16":"lib/forge-std/src/StdCheats.sol","17":"lib/forge-std/src/StdConstants.sol","18":"lib/forge-std/src/StdError.sol","19":"lib/forge-std/src/StdInvariant.sol","20":"lib/forge-std/src/StdJson.sol","21":"lib/forge-std/src/StdMath.sol","22":"lib/forge-std/src/StdStorage.sol","23":"lib/forge-std/src/StdStyle.sol","24":"lib/forge-std/src/StdToml.sol","25":"lib/forge-std/src/StdUtils.sol","26":"lib/forge-std/src/Test.sol","27":"lib/forge-std/src/Vm.sol","28":"lib/forge-std/src/console.sol","29":"lib/forge-std/src/console2.sol","30":"lib/forge-std/src/interfaces/IMulticall3.sol","31":"lib/forge-std/src/safeconsole.sol","32":"node_modules/@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol","33":"node_modules/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol","34":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol","35":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol","36":"node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol","37":"node_modules/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol","38":"node_modules/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol","39":"node_modules/@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol","40":"node_modules/@openzeppelin/contracts/access/Ownable.sol","41":"node_modules/@openzeppelin/contracts/interfaces/IERC1363.sol","42":"node_modules/@openzeppelin/contracts/interfaces/IERC165.sol","43":"node_modules/@openzeppelin/contracts/interfaces/IERC1967.sol","44":"node_modules/@openzeppelin/contracts/interfaces/IERC20.sol","45":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC1822.sol","46":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC6093.sol","47":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol","48":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol","49":"node_modules/@openzeppelin/contracts/proxy/Proxy.sol","50":"node_modules/@openzeppelin/contracts/proxy/beacon/IBeacon.sol","51":"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol","52":"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol","53":"node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol","54":"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol","55":"node_modules/@openzeppelin/contracts/utils/Address.sol","56":"node_modules/@openzeppelin/contracts/utils/Context.sol","57":"node_modules/@openzeppelin/contracts/utils/Errors.sol","58":"node_modules/@openzeppelin/contracts/utils/StorageSlot.sol","59":"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol","60":"test/YtLending.t.sol"},"language":"Solidity"}

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;