From d9b87b2abe58952e00bca956fbc01f26f731b563 Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Sat, 11 May 2024 23:10:30 -0400 Subject: [PATCH 1/2] fix: divide by zero in interest rate module when totalEthSupply is too small now returns minimum base rate instead of reverting --- src/InterestRate.sol | 11 ++++- test/unit/concrete/IonPool.t.sol | 76 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/InterestRate.sol b/src/InterestRate.sol index b6a5b25f..d4c7c841 100644 --- a/src/InterestRate.sol +++ b/src/InterestRate.sol @@ -325,9 +325,16 @@ contract InterestRate { if (distributionFactor == 0) { return (ilkData.minimumKinkRate, ilkData.reserveFactor.scaleUpToRay(4)); } + + uint256 totalEthSupplyScaled = totalEthSupply.wadMulDown(distributionFactor.scaleUpToWad(4)); + // If the `totalEthSupply` is small enough to truncate to zero, then + // simply return the minimum base rate. + if (totalEthSupplyScaled == 0) { + return (ilkData.minimumBaseRate, ilkData.reserveFactor.scaleUpToRay(4)); + } + // [RAD] / [WAD] = [RAY] - uint256 utilizationRate = - totalEthSupply == 0 ? 0 : totalIlkDebt / (totalEthSupply.wadMulDown(distributionFactor.scaleUpToWad(4))); + uint256 utilizationRate = totalEthSupply == 0 ? 0 : totalIlkDebt / totalEthSupplyScaled; // Avoid stack too deep uint256 adjustedBelowKinkSlope; diff --git a/test/unit/concrete/IonPool.t.sol b/test/unit/concrete/IonPool.t.sol index 0863f53a..87b5eb46 100644 --- a/test/unit/concrete/IonPool.t.sol +++ b/test/unit/concrete/IonPool.t.sol @@ -1192,6 +1192,82 @@ contract IonPool_InterestTest is IonPoolSharedSetup, IIonPoolEvents { previousRates[i] = rate; } } + + // If distribution factor is zero, should return + // minimum kink rate. + function test_DivideByZeroWhenDistributionFactorIsZero() public { + IlkData[] memory ilkConfigs = new IlkData[](2); + uint16[] memory distributionFactors = new uint16[](2); + distributionFactors[0] = 0; + distributionFactors[1] = 1e4; + + uint96 minimumKinkRate = 4_062_570_058_138_700_000; + for (uint8 i; i != 2; ++i) { + IlkData memory ilkConfig = IlkData({ + adjustedProfitMargin: 0, + minimumKinkRate: minimumKinkRate, + reserveFactor: 0, + adjustedBaseRate: 0, + minimumBaseRate: 0, + optimalUtilizationRate: 9000, + distributionFactor: distributionFactors[i], + adjustedAboveKinkSlope: 0, + minimumAboveKinkSlope: 0 + }); + ilkConfigs[i] = ilkConfig; + } + + interestRateModule = new InterestRate(ilkConfigs, apyOracle); + + vm.warp(block.timestamp + 1 days); + + (uint256 zeroDistFactorBorrowRate,) = interestRateModule.calculateInterestRate(0, 10e45, 100e18); // 10% + // utilization + assertEq(zeroDistFactorBorrowRate, minimumKinkRate, "borrow rate should be minimum kink rate"); + + (uint256 nonZeroDistFactorBorrowRate,) = interestRateModule.calculateInterestRate(1, 100e45, 100e18); // 90% + // utilization + assertApproxEqAbs( + nonZeroDistFactorBorrowRate, minimumKinkRate, 1, "borrow rate at any util should be minimum kink rate" + ); + } + + // If scaling total eth supply with distribution factor truncates to zero, + // should return minimum base rate. + function test_DivideByZeroWhenTotalEthSupplyIsSmall() public { + IlkData[] memory ilkConfigs = new IlkData[](2); + uint16[] memory distributionFactors = new uint16[](2); + distributionFactors[0] = 0.5e4; + distributionFactors[1] = 0.5e4; + + uint96 minimumKinkRate = 4_062_570_058_138_700_000; + uint96 minimumBaseRate = 1_580_630_071_273_960_000; + for (uint8 i; i != 2; ++i) { + IlkData memory ilkConfig = IlkData({ + adjustedProfitMargin: 0, + minimumKinkRate: minimumKinkRate, + reserveFactor: 0, + adjustedBaseRate: 0, + minimumBaseRate: minimumBaseRate, + optimalUtilizationRate: 9000, + distributionFactor: distributionFactors[i], + adjustedAboveKinkSlope: 0, + minimumAboveKinkSlope: 0 + }); + ilkConfigs[i] = ilkConfig; + } + + interestRateModule = new InterestRate(ilkConfigs, apyOracle); + + vm.warp(block.timestamp + 1 days); + + (uint256 borrowRate,) = interestRateModule.calculateInterestRate(0, 0, 1); // dust amount of eth supply + assertEq(borrowRate, minimumBaseRate, "borrow rate should be minimum base rate"); + + (uint256 borrowRateWithoutTruncation,) = interestRateModule.calculateInterestRate(1, 90e45, 100e18); // 90% + // utilization + assertApproxEqAbs(borrowRateWithoutTruncation, minimumKinkRate, 1, "borrow rate without truncation"); + } } contract IonPool_AdminTest is IonPoolSharedSetup { From 8bdb2098e9cffec7f516f43aadaa00ec239c527c Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Mon, 13 May 2024 14:29:35 -0400 Subject: [PATCH 2/2] fix: instead of early exit, set utilization rate to zero if totalEthSupply scaled by distribution factor truncates to zero --- src/InterestRate.sol | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/InterestRate.sol b/src/InterestRate.sol index d4c7c841..d2f84798 100644 --- a/src/InterestRate.sol +++ b/src/InterestRate.sol @@ -326,15 +326,12 @@ contract InterestRate { return (ilkData.minimumKinkRate, ilkData.reserveFactor.scaleUpToRay(4)); } - uint256 totalEthSupplyScaled = totalEthSupply.wadMulDown(distributionFactor.scaleUpToWad(4)); // If the `totalEthSupply` is small enough to truncate to zero, then - // simply return the minimum base rate. - if (totalEthSupplyScaled == 0) { - return (ilkData.minimumBaseRate, ilkData.reserveFactor.scaleUpToRay(4)); - } + // treat the utilization as zero. + uint256 totalEthSupplyScaled = totalEthSupply.wadMulDown(distributionFactor.scaleUpToWad(4)); // [RAD] / [WAD] = [RAY] - uint256 utilizationRate = totalEthSupply == 0 ? 0 : totalIlkDebt / totalEthSupplyScaled; + uint256 utilizationRate = totalEthSupplyScaled == 0 ? 0 : totalIlkDebt / totalEthSupplyScaled; // Avoid stack too deep uint256 adjustedBelowKinkSlope;