update lending contract and lending test case
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -182,6 +182,7 @@ contract Lending is
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice 取出基础资产(如果余额不足会自动借款)
|
* @notice 取出基础资产(如果余额不足会自动借款)
|
||||||
|
* @dev 如果用户余额不足,会自动借款,借款金额为 amount,借款利率为 borrowRate,借款期限为 borrowPeriod
|
||||||
*/
|
*/
|
||||||
function withdraw(uint256 amount) external override nonReentrant whenNotPaused {
|
function withdraw(uint256 amount) external override nonReentrant whenNotPaused {
|
||||||
accrueInterest();
|
accrueInterest();
|
||||||
@@ -224,6 +225,7 @@ contract Lending is
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice 存入抵押品
|
* @notice 存入抵押品
|
||||||
|
* @dev 由于不涉及债务计算,存入抵押品反而会让账户更安全,所以不用更新利息因子
|
||||||
*/
|
*/
|
||||||
function supplyCollateral(address asset, uint256 amount) external override nonReentrant whenNotPaused {
|
function supplyCollateral(address asset, uint256 amount) external override nonReentrant whenNotPaused {
|
||||||
AssetConfig memory config = assetConfigs[asset];
|
AssetConfig memory config = assetConfigs[asset];
|
||||||
@@ -272,8 +274,6 @@ contract Lending is
|
|||||||
int104 oldPrincipal = user.principal;
|
int104 oldPrincipal = user.principal;
|
||||||
|
|
||||||
// 计算当前实际余额(含利息)
|
// 计算当前实际余额(含利息)
|
||||||
// 如果 principal >= 0(存款),使用 supplyIndex
|
|
||||||
// 如果 principal < 0(借款),使用 borrowIndex
|
|
||||||
uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
|
uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
|
||||||
int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
|
int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
|
||||||
|
|
||||||
@@ -370,13 +370,19 @@ contract Lending is
|
|||||||
// 计算偿还和供应金额
|
// 计算偿还和供应金额
|
||||||
(uint104 repayAmount, uint104 supplyAmount) = LendingMath.repayAndSupplyAmount(oldPrincipal, newPrincipal);
|
(uint104 repayAmount, uint104 supplyAmount) = LendingMath.repayAndSupplyAmount(oldPrincipal, newPrincipal);
|
||||||
|
|
||||||
// 更新全局状态
|
// 更新全局状态(储备金通过减少 totalBorrowBase 和增加 totalSupplyBase 来承担坏账)
|
||||||
// 储备金通过减少 totalBorrowBase 和增加 totalSupplyBase 来承担坏账
|
|
||||||
totalSupplyBase += supplyAmount;
|
totalSupplyBase += supplyAmount;
|
||||||
totalBorrowBase -= repayAmount;
|
totalBorrowBase -= repayAmount;
|
||||||
|
|
||||||
// 计算协议支付的债务(坏账部分)
|
// 计算协议承担的坏账部分
|
||||||
uint256 basePaidOut = uint256(newBalance - oldBalance);
|
// 坏账 = 用户债务 - 抵押品价值(当抵押品不足时)
|
||||||
|
uint256 basePaidOut = 0;
|
||||||
|
if (int256(collateralInBase) < -oldBalance) {
|
||||||
|
// 抵押品不足以覆盖债务,差额由协议储备金承担
|
||||||
|
basePaidOut = uint256(-oldBalance) - collateralInBase;
|
||||||
|
}
|
||||||
|
// 如果 collateralInBase >= -oldBalance,说明抵押品足够,无坏账
|
||||||
|
|
||||||
uint256 valueOfBasePaidOut = (basePaidOut * basePrice) / baseScale;
|
uint256 valueOfBasePaidOut = (basePaidOut * basePrice) / baseScale;
|
||||||
|
|
||||||
// 发射债务吸收事件
|
// 发射债务吸收事件
|
||||||
@@ -513,18 +519,6 @@ contract Lending is
|
|||||||
return totalValue;
|
return totalValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @notice 获取最小借款抵押率
|
|
||||||
*/
|
|
||||||
function _getMinBorrowCollateralFactor() internal view returns (uint64) {
|
|
||||||
uint64 minFactor = type(uint64).max;
|
|
||||||
for (uint i = 0; i < assetList.length; i++) {
|
|
||||||
uint64 factor = assetConfigs[assetList[i]].borrowCollateralFactor;
|
|
||||||
if (factor < minFactor) minFactor = factor;
|
|
||||||
}
|
|
||||||
return minFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== View Functions ==========
|
// ========== View Functions ==========
|
||||||
|
|
||||||
function getBalance(address account) external view override returns (int256) {
|
function getBalance(address account) external view override returns (int256) {
|
||||||
|
|||||||
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
1
out/build-info/4d57c48057fc3574.json
Normal file
1
out/build-info/4d57c48057fc3574.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":"4d57c48057fc3574","source_id_to_path":{"0":"contracts/ytLending/Configurator.sol","1":"contracts/ytLending/ConfiguratorStorage.sol","2":"contracts/ytLending/Lending.sol","3":"contracts/ytLending/LendingConfiguration.sol","4":"contracts/ytLending/LendingFactory.sol","5":"contracts/ytLending/LendingMath.sol","6":"contracts/ytLending/LendingStorage.sol","7":"contracts/ytLending/interfaces/ILending.sol","8":"contracts/ytLending/interfaces/IPriceFeed.sol","9":"lib/forge-std/src/Base.sol","10":"lib/forge-std/src/StdAssertions.sol","11":"lib/forge-std/src/StdChains.sol","12":"lib/forge-std/src/StdCheats.sol","13":"lib/forge-std/src/StdConstants.sol","14":"lib/forge-std/src/StdError.sol","15":"lib/forge-std/src/StdInvariant.sol","16":"lib/forge-std/src/StdJson.sol","17":"lib/forge-std/src/StdMath.sol","18":"lib/forge-std/src/StdStorage.sol","19":"lib/forge-std/src/StdStyle.sol","20":"lib/forge-std/src/StdToml.sol","21":"lib/forge-std/src/StdUtils.sol","22":"lib/forge-std/src/Test.sol","23":"lib/forge-std/src/Vm.sol","24":"lib/forge-std/src/console.sol","25":"lib/forge-std/src/console2.sol","26":"lib/forge-std/src/interfaces/IMulticall3.sol","27":"lib/forge-std/src/safeconsole.sol","28":"node_modules/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol","29":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol","30":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol","31":"node_modules/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol","32":"node_modules/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol","33":"node_modules/@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol","34":"node_modules/@openzeppelin/contracts/access/Ownable.sol","35":"node_modules/@openzeppelin/contracts/interfaces/IERC1363.sol","36":"node_modules/@openzeppelin/contracts/interfaces/IERC165.sol","37":"node_modules/@openzeppelin/contracts/interfaces/IERC1967.sol","38":"node_modules/@openzeppelin/contracts/interfaces/IERC20.sol","39":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC1822.sol","40":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC6093.sol","41":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol","42":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol","43":"node_modules/@openzeppelin/contracts/proxy/Proxy.sol","44":"node_modules/@openzeppelin/contracts/proxy/beacon/IBeacon.sol","45":"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol","46":"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol","47":"node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol","48":"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol","49":"node_modules/@openzeppelin/contracts/utils/Address.sol","50":"node_modules/@openzeppelin/contracts/utils/Context.sol","51":"node_modules/@openzeppelin/contracts/utils/Errors.sol","52":"node_modules/@openzeppelin/contracts/utils/StorageSlot.sol","53":"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol","54":"test/YtLending.t.sol"},"language":"Solidity"}
|
||||||
1
out/build-info/685284636136dbec.json
Normal file
1
out/build-info/685284636136dbec.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":"685284636136dbec","source_id_to_path":{"0":"contracts/ytLending/Configurator.sol","1":"contracts/ytLending/ConfiguratorStorage.sol","2":"contracts/ytLending/Lending.sol","3":"contracts/ytLending/LendingConfiguration.sol","4":"contracts/ytLending/LendingFactory.sol","5":"contracts/ytLending/LendingMath.sol","6":"contracts/ytLending/LendingStorage.sol","7":"contracts/ytLending/interfaces/ILending.sol","8":"contracts/ytLending/interfaces/IPriceFeed.sol","9":"lib/forge-std/src/Base.sol","10":"lib/forge-std/src/StdAssertions.sol","11":"lib/forge-std/src/StdChains.sol","12":"lib/forge-std/src/StdCheats.sol","13":"lib/forge-std/src/StdConstants.sol","14":"lib/forge-std/src/StdError.sol","15":"lib/forge-std/src/StdInvariant.sol","16":"lib/forge-std/src/StdJson.sol","17":"lib/forge-std/src/StdMath.sol","18":"lib/forge-std/src/StdStorage.sol","19":"lib/forge-std/src/StdStyle.sol","20":"lib/forge-std/src/StdToml.sol","21":"lib/forge-std/src/StdUtils.sol","22":"lib/forge-std/src/Test.sol","23":"lib/forge-std/src/Vm.sol","24":"lib/forge-std/src/console.sol","25":"lib/forge-std/src/console2.sol","26":"lib/forge-std/src/interfaces/IMulticall3.sol","27":"lib/forge-std/src/safeconsole.sol","28":"node_modules/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol","29":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol","30":"node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol","31":"node_modules/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol","32":"node_modules/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol","33":"node_modules/@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol","34":"node_modules/@openzeppelin/contracts/access/Ownable.sol","35":"node_modules/@openzeppelin/contracts/interfaces/IERC1363.sol","36":"node_modules/@openzeppelin/contracts/interfaces/IERC165.sol","37":"node_modules/@openzeppelin/contracts/interfaces/IERC1967.sol","38":"node_modules/@openzeppelin/contracts/interfaces/IERC20.sol","39":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC1822.sol","40":"node_modules/@openzeppelin/contracts/interfaces/draft-IERC6093.sol","41":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol","42":"node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol","43":"node_modules/@openzeppelin/contracts/proxy/Proxy.sol","44":"node_modules/@openzeppelin/contracts/proxy/beacon/IBeacon.sol","45":"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol","46":"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol","47":"node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol","48":"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol","49":"node_modules/@openzeppelin/contracts/utils/Address.sol","50":"node_modules/@openzeppelin/contracts/utils/Context.sol","51":"node_modules/@openzeppelin/contracts/utils/Errors.sol","52":"node_modules/@openzeppelin/contracts/utils/StorageSlot.sol","53":"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol","54":"test/YtLending.t.sol"},"language":"Solidity"}
|
||||||
@@ -321,15 +321,19 @@ contract YtLendingTest is Test {
|
|||||||
lending.accrueInterest();
|
lending.accrueInterest();
|
||||||
|
|
||||||
// 利用率 = 8000 / 10000 = 80%(在 kink 点)
|
// 利用率 = 8000 / 10000 = 80%(在 kink 点)
|
||||||
// Supply APY = 3%(在 kink 点)
|
// Supply APY 计算:
|
||||||
// 预期余额 ≈ 10,000 * 1.03 = 10,300 USDC
|
// rate = base + (utilization × slope)
|
||||||
|
// = 0% + (80% × 3%) = 2.4%
|
||||||
|
// 预期余额 = 10,000 × 1.024 = 10,240 USDC
|
||||||
uint256 aliceBalance = lending.balanceOf(alice);
|
uint256 aliceBalance = lending.balanceOf(alice);
|
||||||
assertApproxEqRel(aliceBalance, 10300e18, 0.01e18, "Alice should earn ~3% interest");
|
assertApproxEqRel(aliceBalance, 10240e18, 0.001e18, "Alice should earn 2.4% interest (0.1% tolerance)");
|
||||||
|
|
||||||
// Borrow APY = 1.5% + 5% = 6.5%(在 kink 点)
|
// Borrow APY 计算:
|
||||||
// 预期债务 ≈ 8,000 * 1.065 = 8,520 USDC
|
// rate = base + (utilization × slope)
|
||||||
|
// = 1.5% + (80% × 5%) = 5.5%
|
||||||
|
// 预期债务 = 8,000 × 1.055 = 8,440 USDC
|
||||||
uint256 bobDebt = lending.borrowBalanceOf(bob);
|
uint256 bobDebt = lending.borrowBalanceOf(bob);
|
||||||
assertApproxEqRel(bobDebt, 8520e18, 0.01e18, "Bob should owe ~6.5% interest");
|
assertApproxEqRel(bobDebt, 8440e18, 0.001e18, "Bob should owe 5.5% interest (0.1% tolerance)");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_11_InterestAccrual_Compound() public {
|
function test_11_InterestAccrual_Compound() public {
|
||||||
@@ -399,7 +403,52 @@ contract YtLendingTest is Test {
|
|||||||
assertTrue(lending.isLiquidatable(bob), "Bob should be liquidatable");
|
assertTrue(lending.isLiquidatable(bob), "Bob should be liquidatable");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_14_Absorb_Single() public {
|
function test_14_Liquidation_AtExactThreshold() public {
|
||||||
|
// 这个测试验证:在刚好达到清算线时就可以被清算
|
||||||
|
|
||||||
|
// 0. Alice 先存入流动性
|
||||||
|
vm.prank(alice);
|
||||||
|
lending.supply(50000e18);
|
||||||
|
|
||||||
|
// 1. Bob 建立借款头寸
|
||||||
|
vm.startPrank(bob);
|
||||||
|
lending.supplyCollateral(address(weth), 10e18); // 10 ETH @ $2000 = $20,000
|
||||||
|
lending.borrow(16000e18); // $16,000(80% LTV)
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// 2. 计算精确的清算价格
|
||||||
|
// 清算条件(在 Solidity 整数运算中):
|
||||||
|
// debtValue > collateralValue × liquidateCollateralFactor
|
||||||
|
// 16000e8 > (10e18 × price_e8 / 1e18) × 0.85e18 / 1e18
|
||||||
|
// 16000e8 > price_e8 × 8.5
|
||||||
|
// price_e8 < 16000e8 / 8.5 = 188235294117.647...
|
||||||
|
// 安全价格:price_e8 >= 188235294118($1,882.35294118)
|
||||||
|
|
||||||
|
// 在清算阈值之上(安全)
|
||||||
|
wethPriceFeed.setPrice(1883e8); // $1,883
|
||||||
|
assertFalse(lending.isLiquidatable(bob), "Bob should be safe at $1,883");
|
||||||
|
|
||||||
|
// 接近但仍在清算阈值之上
|
||||||
|
wethPriceFeed.setPrice(188235294118); // $1,882.35294118(安全临界点)
|
||||||
|
assertFalse(lending.isLiquidatable(bob), "Bob should be at the safe edge");
|
||||||
|
|
||||||
|
// 刚好跌破清算阈值
|
||||||
|
wethPriceFeed.setPrice(188235294117); // $1,882.35294117(危险)
|
||||||
|
// collateralValue = 188235294117 × 8.5 = 1,599,999,999,994.5
|
||||||
|
// debtValue = 16000e8 = 1,600,000,000,000
|
||||||
|
// 1,600,000,000,000 > 1,599,999,999,994 ✅ 可清算
|
||||||
|
assertTrue(lending.isLiquidatable(bob), "Bob should be liquidatable just below threshold");
|
||||||
|
|
||||||
|
// 3. 执行清算
|
||||||
|
vm.prank(liquidator);
|
||||||
|
lending.absorb(bob);
|
||||||
|
|
||||||
|
// 4. 验证清算成功
|
||||||
|
assertEq(lending.getCollateral(bob, address(weth)), 0, "Bob's collateral should be seized");
|
||||||
|
assertEq(lending.getCollateralReserves(address(weth)), 10e18, "Collateral should be in reserves");
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_15_Absorb_Single() public {
|
||||||
// 0. Alice 先存入流动性
|
// 0. Alice 先存入流动性
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
@@ -436,7 +485,7 @@ contract YtLendingTest is Test {
|
|||||||
assertTrue(lending.balanceOf(bob) > 0, "Bob should have positive balance from excess collateral");
|
assertTrue(lending.balanceOf(bob) > 0, "Bob should have positive balance from excess collateral");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_15_AbsorbMultiple_Batch() public {
|
function test_16_AbsorbMultiple_Batch() public {
|
||||||
// 0. Alice 先存入流动性
|
// 0. Alice 先存入流动性
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
@@ -473,7 +522,7 @@ contract YtLendingTest is Test {
|
|||||||
BUY COLLATERAL 测试
|
BUY COLLATERAL 测试
|
||||||
//////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
function test_16_BuyCollateral_Basic() public {
|
function test_17_BuyCollateral_Basic() public {
|
||||||
// 0. Alice 先存入流动性
|
// 0. Alice 先存入流动性
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
@@ -511,7 +560,7 @@ contract YtLendingTest is Test {
|
|||||||
assertEq(lending.getCollateralReserves(address(weth)), 0, "Collateral reserve should be empty");
|
assertEq(lending.getCollateralReserves(address(weth)), 0, "Collateral reserve should be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_17_BuyCollateral_WithRecipient() public {
|
function test_18_BuyCollateral_WithRecipient() public {
|
||||||
// 先存入流动性
|
// 先存入流动性
|
||||||
vm.prank(owner);
|
vm.prank(owner);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
@@ -535,7 +584,7 @@ contract YtLendingTest is Test {
|
|||||||
assertEq(weth.balanceOf(alice), 60e18, "Alice should receive the ETH (50 + 10)");
|
assertEq(weth.balanceOf(alice), 60e18, "Alice should receive the ETH (50 + 10)");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_18_BuyCollateral_FailWhenReserveSufficient() public {
|
function test_19_BuyCollateral_FailWhenReserveSufficient() public {
|
||||||
// 这个测试验证:当 reserves >= targetReserves 时,不能购买抵押品
|
// 这个测试验证:当 reserves >= targetReserves 时,不能购买抵押品
|
||||||
// 为简化测试,我们直接验证 buyCollateral 的逻辑
|
// 为简化测试,我们直接验证 buyCollateral 的逻辑
|
||||||
|
|
||||||
@@ -586,12 +635,12 @@ contract YtLendingTest is Test {
|
|||||||
RESERVES 测试
|
RESERVES 测试
|
||||||
//////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
function test_19_GetReserves_Initial() public view {
|
function test_20_GetReserves_Initial() public view {
|
||||||
// 初始储备金应该是 0
|
// 初始储备金应该是 0
|
||||||
assertEq(lending.getReserves(), 0, "Initial reserves should be 0");
|
assertEq(lending.getReserves(), 0, "Initial reserves should be 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_20_GetReserves_AfterSupplyBorrow() public {
|
function test_21_GetReserves_AfterSupplyBorrow() public {
|
||||||
// Alice 存入 10,000 USDC
|
// Alice 存入 10,000 USDC
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(10000e18);
|
lending.supply(10000e18);
|
||||||
@@ -610,7 +659,7 @@ contract YtLendingTest is Test {
|
|||||||
assertEq(lending.getReserves(), 0, "Reserves should still be 0");
|
assertEq(lending.getReserves(), 0, "Reserves should still be 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_21_GetReserves_WithInterest() public {
|
function test_22_GetReserves_WithInterest() public {
|
||||||
// 建立借贷
|
// 建立借贷
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(10000e18);
|
lending.supply(10000e18);
|
||||||
@@ -625,15 +674,19 @@ contract YtLendingTest is Test {
|
|||||||
lending.accrueInterest();
|
lending.accrueInterest();
|
||||||
|
|
||||||
// 借款利率 > 存款利率,reserves 应该增加
|
// 借款利率 > 存款利率,reserves 应该增加
|
||||||
// Borrow APY ≈ 6.5%, Supply APY ≈ 3%
|
// 利用率 = 80%(在 kink 点)
|
||||||
// 利差 ≈ 8000 * 0.065 - 10000 * 0.03 = 520 - 300 = 220
|
// Supply APY = 0% + 80% × 3% = 2.4%
|
||||||
// 由于利率是按秒计算,会有舍入误差
|
// Borrow APY = 1.5% + 80% × 5% = 5.5%
|
||||||
|
//
|
||||||
|
// Alice 存款利息 = 10,000 × 2.4% = 240 USDC
|
||||||
|
// Bob 借款利息 = 8,000 × 5.5% = 440 USDC
|
||||||
|
// 储备金增加 = 440 - 240 = 200 USDC
|
||||||
int256 reserves = lending.getReserves();
|
int256 reserves = lending.getReserves();
|
||||||
assertTrue(reserves > 0, "Reserves should be positive from interest spread");
|
assertTrue(reserves > 0, "Reserves should be positive from interest spread");
|
||||||
assertApproxEqRel(uint256(reserves), 220e18, 0.15e18, "Reserves should be ~220 USDC");
|
assertApproxEqRel(uint256(reserves), 200e18, 0.005e18, "Reserves should be 200 USDC (0.5% tolerance)");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_22_WithdrawReserves_Success() public {
|
function test_23_WithdrawReserves_Success() public {
|
||||||
// 1. 累积储备金
|
// 1. 累积储备金
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(10000e18);
|
lending.supply(10000e18);
|
||||||
@@ -665,13 +718,13 @@ contract YtLendingTest is Test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_23_WithdrawReserves_FailInsufficientReserves() public {
|
function test_24_WithdrawReserves_FailInsufficientReserves() public {
|
||||||
// 尝试提取不存在的储备金
|
// 尝试提取不存在的储备金
|
||||||
vm.expectRevert(ILending.InsufficientReserves.selector);
|
vm.expectRevert(ILending.InsufficientReserves.selector);
|
||||||
lending.withdrawReserves(address(0x999), 1000e18);
|
lending.withdrawReserves(address(0x999), 1000e18);
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_24_WithdrawReserves_FailNotOwner() public {
|
function test_25_WithdrawReserves_FailNotOwner() public {
|
||||||
// 非 owner 尝试提取
|
// 非 owner 尝试提取
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
vm.expectRevert();
|
vm.expectRevert();
|
||||||
@@ -682,7 +735,7 @@ contract YtLendingTest is Test {
|
|||||||
VIEW FUNCTIONS 测试
|
VIEW FUNCTIONS 测试
|
||||||
//////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
function test_25_GetUtilization() public {
|
function test_26_GetUtilization() public {
|
||||||
// 初始利用率应该是 0
|
// 初始利用率应该是 0
|
||||||
assertEq(lending.getUtilization(), 0, "Initial utilization should be 0");
|
assertEq(lending.getUtilization(), 0, "Initial utilization should be 0");
|
||||||
|
|
||||||
@@ -700,7 +753,7 @@ contract YtLendingTest is Test {
|
|||||||
assertEq(lending.getUtilization(), 0.8e18, "Utilization should be 80%");
|
assertEq(lending.getUtilization(), 0.8e18, "Utilization should be 80%");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_26_GetSupplyRate_BelowKink() public {
|
function test_27_GetSupplyRate_BelowKink() public {
|
||||||
// 利用率 50%,低于 kink(80%)
|
// 利用率 50%,低于 kink(80%)
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(10000e18);
|
lending.supply(10000e18);
|
||||||
@@ -712,12 +765,13 @@ contract YtLendingTest is Test {
|
|||||||
|
|
||||||
uint64 supplyRate = lending.getSupplyRate();
|
uint64 supplyRate = lending.getSupplyRate();
|
||||||
|
|
||||||
// 预期:base + 50% * slopeLow = 0 + 0.5 * 3% = 1.5% APY
|
// 预期:base + utilization × slopeLow
|
||||||
// 但是转换为每秒:1.5% / 31536000 ≈ 0.475e9
|
// = 0% + 50% × 3% = 1.5% APY
|
||||||
assertApproxEqRel(supplyRate, 0.015e18, 0.01e18, "Supply rate should be ~1.5% APY");
|
// 这是简单计算,应该非常精确
|
||||||
|
assertApproxEqRel(supplyRate, 0.015e18, 0.0001e18, "Supply rate should be 1.5% APY (0.01% tolerance)");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_27_GetBorrowRate_AtKink() public {
|
function test_28_GetBorrowRate_AtKink() public {
|
||||||
// 利用率正好 80%
|
// 利用率正好 80%
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(10000e18);
|
lending.supply(10000e18);
|
||||||
@@ -729,12 +783,14 @@ contract YtLendingTest is Test {
|
|||||||
|
|
||||||
uint64 borrowRate = lending.getBorrowRate();
|
uint64 borrowRate = lending.getBorrowRate();
|
||||||
|
|
||||||
// 预期:base + slopeLow = 1.5% + 5% = 6.5% APY
|
// 预期:base + utilization × slopeLow
|
||||||
// 实际:由于是线性插值 + 按秒计算再转年化,会有小的舍入误差
|
// = 1.5% + 80% × 5%
|
||||||
assertApproxEqRel(borrowRate, 0.065e18, 0.2e18, "Borrow rate should be ~6.5% APY");
|
// = 1.5% + 4% = 5.5% APY
|
||||||
|
// 注:getBorrowRate() 返回的是年化利率,精度很高
|
||||||
|
assertApproxEqRel(borrowRate, 0.055e18, 0.0001e18, "Borrow rate should be 5.5% APY (0.01% tolerance)");
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_28_QuoteCollateral() public view {
|
function test_29_QuoteCollateral() public view {
|
||||||
// ETH 价格 $2000, liquidationFactor 0.95, storeFrontFactor 0.5
|
// ETH 价格 $2000, liquidationFactor 0.95, storeFrontFactor 0.5
|
||||||
// discount = 0.5 * (1 - 0.95) = 0.025 (2.5%)
|
// discount = 0.5 * (1 - 0.95) = 0.025 (2.5%)
|
||||||
// 折扣价 = 2000 * (1 - 0.025) = $1,950
|
// 折扣价 = 2000 * (1 - 0.025) = $1,950
|
||||||
@@ -750,7 +806,7 @@ contract YtLendingTest is Test {
|
|||||||
EDGE CASES 测试
|
EDGE CASES 测试
|
||||||
//////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
function test_29_Borrow_MaxLTV() public {
|
function test_30_Borrow_MaxLTV() public {
|
||||||
// Bob 先存入流动性
|
// Bob 先存入流动性
|
||||||
vm.prank(bob);
|
vm.prank(bob);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
@@ -767,7 +823,7 @@ contract YtLendingTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_30_Borrow_FailOverLTV() public {
|
function test_31_Borrow_FailOverLTV() public {
|
||||||
// Bob 先存入流动性
|
// Bob 先存入流动性
|
||||||
vm.prank(bob);
|
vm.prank(bob);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
@@ -782,7 +838,7 @@ contract YtLendingTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_31_WithdrawCollateral_FailIfBorrowing() public {
|
function test_32_WithdrawCollateral_FailIfBorrowing() public {
|
||||||
// Bob 先存入流动性
|
// Bob 先存入流动性
|
||||||
vm.prank(bob);
|
vm.prank(bob);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
@@ -798,7 +854,7 @@ contract YtLendingTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_32_SupplyCollateral_FailExceedCap() public {
|
function test_33_SupplyCollateral_FailExceedCap() public {
|
||||||
// 尝试超过供应上限(100,000 ETH)
|
// 尝试超过供应上限(100,000 ETH)
|
||||||
weth.mint(alice, 200000e18);
|
weth.mint(alice, 200000e18);
|
||||||
|
|
||||||
@@ -808,7 +864,7 @@ contract YtLendingTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_33_ComplexScenario_MultipleUsers() public {
|
function test_34_ComplexScenario_MultipleUsers() public {
|
||||||
// 1. Alice 存款
|
// 1. Alice 存款
|
||||||
vm.prank(alice);
|
vm.prank(alice);
|
||||||
lending.supply(50000e18);
|
lending.supply(50000e18);
|
||||||
|
|||||||
Reference in New Issue
Block a user