From b44ae2c380d4470c503836a7ca5f60c34b810d8d Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 10 Nov 2023 14:40:41 +0100 Subject: [PATCH 1/7] fix(constructor): add parameter bounds --- lib/morpho-blue | 2 +- src/SpeedJumpIrm.sol | 22 +++++++++---------- src/libraries/AdaptativeCurveIrmLib.sol | 16 ++++++++++++++ test/SpeedJumpIrmTest.sol | 28 ++++++++++++++++++------- 4 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 src/libraries/AdaptativeCurveIrmLib.sol diff --git a/lib/morpho-blue b/lib/morpho-blue index f463e40f..5e515a60 160000 --- a/lib/morpho-blue +++ b/lib/morpho-blue @@ -1 +1 @@ -Subproject commit f463e40f776acd0f26d0d380b51cfd02949c8c23 +Subproject commit 5e515a607a24dcaa04ebf4dcce8e49419353e5aa diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index f831a1f5..67451055 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -6,6 +6,7 @@ import {IIrm} from "../lib/morpho-blue/src/interfaces/IIrm.sol"; import {UtilsLib} from "./libraries/UtilsLib.sol"; import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {MathLib, WAD_INT as WAD} from "./libraries/MathLib.sol"; +import {AdaptativeCurveIrmLib} from "./libraries/AdaptativeCurveIrmLib.sol"; import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib.sol"; import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; @@ -27,19 +28,15 @@ contract AdaptativeCurveIrm is IIrm { /* CONSTANTS */ - /// @notice Maximum rate at target per second (scaled by WAD) (1B% APR). - int256 public constant MAX_RATE_AT_TARGET = int256(0.01e9 ether) / 365 days; - /// @notice Mininimum rate at target per second (scaled by WAD) (0.1% APR). - int256 public constant MIN_RATE_AT_TARGET = int256(0.001 ether) / 365 days; /// @notice Address of Morpho. address public immutable MORPHO; /// @notice Curve steepness (scaled by WAD). - /// @dev Verified to be greater than 1 at construction. + /// @dev Verified to be inside the expected range at construction. int256 public immutable CURVE_STEEPNESS; /// @notice Adjustment speed (scaled by WAD). /// @dev The speed is per second, so the rate moves at a speed of ADJUSTMENT_SPEED * err each second (while being - /// continuously compounded). A typical value for the ADJUSTMENT_SPEED would be 10 ethers / 365 days. - /// @dev Verified to be non-negative at construction. + /// continuously compounded). A typical value for the ADJUSTMENT_SPEED would be 10 ether / 365 days. + /// @dev Verified to be inside the expected range at construction. int256 public immutable ADJUSTMENT_SPEED; /// @notice Target utilization (scaled by WAD). /// @dev Verified to be strictly between 0 and 1 at construction. @@ -71,11 +68,13 @@ contract AdaptativeCurveIrm is IIrm { ) { require(morpho != address(0), ErrorsLib.ZERO_ADDRESS); require(curveSteepness >= WAD, ErrorsLib.INPUT_TOO_SMALL); + require(curveSteepness <= AdaptativeCurveIrmLib.MAX_CURVE_STEEPNESS, ErrorsLib.INPUT_TOO_LARGE); require(adjustmentSpeed >= 0, ErrorsLib.INPUT_TOO_SMALL); + require(adjustmentSpeed <= AdaptativeCurveIrmLib.MAX_ADJUSTMENT_SPEED, ErrorsLib.INPUT_TOO_LARGE); require(targetUtilization < WAD, ErrorsLib.INPUT_TOO_LARGE); require(targetUtilization > 0, ErrorsLib.ZERO_INPUT); - require(initialRateAtTarget >= MIN_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_SMALL); - require(initialRateAtTarget <= MAX_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_LARGE); + require(initialRateAtTarget >= AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_SMALL); + require(initialRateAtTarget <= AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_LARGE); MORPHO = morpho; CURVE_STEEPNESS = curveSteepness; @@ -134,8 +133,9 @@ contract AdaptativeCurveIrm is IIrm { int256 linearAdaptation = speed * elapsed; int256 adaptationMultiplier = MathLib.wExp(linearAdaptation); // endRateAtTarget is bounded between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET. - int256 endRateAtTarget = - startRateAtTarget.wMulDown(adaptationMultiplier).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); + int256 endRateAtTarget = startRateAtTarget.wMulDown(adaptationMultiplier).bound( + AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET, AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET + ); int256 endBorrowRate = _curve(endRateAtTarget, err); // Then we compute the average rate over the period. diff --git a/src/libraries/AdaptativeCurveIrmLib.sol b/src/libraries/AdaptativeCurveIrmLib.sol new file mode 100644 index 00000000..b492d9aa --- /dev/null +++ b/src/libraries/AdaptativeCurveIrmLib.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +library AdaptativeCurveIrmLib { + /// @notice Maximum rate at target per second (scaled by WAD) (1B% APR). + int256 internal constant MAX_RATE_AT_TARGET = int256(0.01e9 ether) / 365 days; + + /// @notice Mininimum rate at target per second (scaled by WAD) (0.1% APR). + int256 internal constant MIN_RATE_AT_TARGET = int256(0.001 ether) / 365 days; + + /// @notice Maximum curve steepness allowed (scaled by WAD). + int256 internal constant MAX_CURVE_STEEPNESS = 100 ether; + + /// @notice Maximum adjustment speed allowed (scaled by WAD). + int256 internal constant MAX_ADJUSTMENT_SPEED = int256(1_000 ether) / 365 days; +} diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index 57d84e24..bc83bd12 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -216,8 +216,8 @@ contract AdaptativeCurveIrmTest is Test { assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); } - function testWExpWMulDownMaxRate() public view { - MathLib.wExp(MathLib.WEXP_UPPER_BOUND).wMulDown(irm.MAX_RATE_AT_TARGET()); + function testWExpWMulDownMaxRate() public pure { + MathLib.wExp(MathLib.WEXP_UPPER_BOUND).wMulDown(AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET); } /* HANDLERS */ @@ -243,8 +243,14 @@ contract AdaptativeCurveIrmTest is Test { market.totalBorrowAssets = 9 ether; market.totalSupplyAssets = 10 ether; - assertGe(irm.borrowRateView(marketParams, market), uint256(irm.MIN_RATE_AT_TARGET().wDivDown(CURVE_STEEPNESS))); - assertGe(irm.borrowRate(marketParams, market), uint256(irm.MIN_RATE_AT_TARGET().wDivDown(CURVE_STEEPNESS))); + assertGe( + irm.borrowRateView(marketParams, market), + uint256(AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) + ); + assertGe( + irm.borrowRate(marketParams, market), + uint256(AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) + ); } function invariantLeMaxRateAtTarget() public { @@ -252,8 +258,14 @@ contract AdaptativeCurveIrmTest is Test { market.totalBorrowAssets = 9 ether; market.totalSupplyAssets = 10 ether; - assertLe(irm.borrowRateView(marketParams, market), uint256(irm.MAX_RATE_AT_TARGET().wMulDown(CURVE_STEEPNESS))); - assertLe(irm.borrowRate(marketParams, market), uint256(irm.MAX_RATE_AT_TARGET().wMulDown(CURVE_STEEPNESS))); + assertLe( + irm.borrowRateView(marketParams, market), + uint256(AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) + ); + assertLe( + irm.borrowRate(marketParams, market), + uint256(AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) + ); } function _expectedRateAtTarget(Id id, Market memory market) internal view returns (int256) { @@ -263,7 +275,9 @@ contract AdaptativeCurveIrmTest is Test { int256 linearAdaptation = speed * int256(elapsed); int256 adaptationMultiplier = MathLib.wExp(linearAdaptation); return (rateAtTarget > 0) - ? rateAtTarget.wMulDown(adaptationMultiplier).bound(irm.MIN_RATE_AT_TARGET(), irm.MAX_RATE_AT_TARGET()) + ? rateAtTarget.wMulDown(adaptationMultiplier).bound( + AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET, AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET + ) : INITIAL_RATE_AT_TARGET; } From 77acf9511309a5bf0e85df38fb8e2133ade52a5f Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 10 Nov 2023 15:42:59 +0100 Subject: [PATCH 2/7] refactor(constants): rename adaptative curve irm lib --- src/SpeedJumpIrm.sol | 18 +++++++++++------- test/SpeedJumpIrmTest.sol | 16 ++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 67451055..b77773fa 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -6,7 +6,7 @@ import {IIrm} from "../lib/morpho-blue/src/interfaces/IIrm.sol"; import {UtilsLib} from "./libraries/UtilsLib.sol"; import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {MathLib, WAD_INT as WAD} from "./libraries/MathLib.sol"; -import {AdaptativeCurveIrmLib} from "./libraries/AdaptativeCurveIrmLib.sol"; +import {AdaptativeCurveIrmLib as ConstantsLib} from "./libraries/AdaptativeCurveIrmLib.sol"; import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib.sol"; import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; @@ -26,21 +26,25 @@ contract AdaptativeCurveIrm is IIrm { /// @notice Emitted when a borrow rate is updated. event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 rateAtTarget); - /* CONSTANTS */ + /* IMMUTABLES */ /// @notice Address of Morpho. address public immutable MORPHO; + /// @notice Curve steepness (scaled by WAD). /// @dev Verified to be inside the expected range at construction. int256 public immutable CURVE_STEEPNESS; + /// @notice Adjustment speed (scaled by WAD). /// @dev The speed is per second, so the rate moves at a speed of ADJUSTMENT_SPEED * err each second (while being /// continuously compounded). A typical value for the ADJUSTMENT_SPEED would be 10 ether / 365 days. /// @dev Verified to be inside the expected range at construction. int256 public immutable ADJUSTMENT_SPEED; + /// @notice Target utilization (scaled by WAD). /// @dev Verified to be strictly between 0 and 1 at construction. int256 public immutable TARGET_UTILIZATION; + /// @notice Initial rate at target per second (scaled by WAD). /// @dev Verified to be between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET at contruction. int256 public immutable INITIAL_RATE_AT_TARGET; @@ -68,13 +72,13 @@ contract AdaptativeCurveIrm is IIrm { ) { require(morpho != address(0), ErrorsLib.ZERO_ADDRESS); require(curveSteepness >= WAD, ErrorsLib.INPUT_TOO_SMALL); - require(curveSteepness <= AdaptativeCurveIrmLib.MAX_CURVE_STEEPNESS, ErrorsLib.INPUT_TOO_LARGE); + require(curveSteepness <= ConstantsLib.MAX_CURVE_STEEPNESS, ErrorsLib.INPUT_TOO_LARGE); require(adjustmentSpeed >= 0, ErrorsLib.INPUT_TOO_SMALL); - require(adjustmentSpeed <= AdaptativeCurveIrmLib.MAX_ADJUSTMENT_SPEED, ErrorsLib.INPUT_TOO_LARGE); + require(adjustmentSpeed <= ConstantsLib.MAX_ADJUSTMENT_SPEED, ErrorsLib.INPUT_TOO_LARGE); require(targetUtilization < WAD, ErrorsLib.INPUT_TOO_LARGE); require(targetUtilization > 0, ErrorsLib.ZERO_INPUT); - require(initialRateAtTarget >= AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_SMALL); - require(initialRateAtTarget <= AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_LARGE); + require(initialRateAtTarget >= ConstantsLib.MIN_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_SMALL); + require(initialRateAtTarget <= ConstantsLib.MAX_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_LARGE); MORPHO = morpho; CURVE_STEEPNESS = curveSteepness; @@ -134,7 +138,7 @@ contract AdaptativeCurveIrm is IIrm { int256 adaptationMultiplier = MathLib.wExp(linearAdaptation); // endRateAtTarget is bounded between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET. int256 endRateAtTarget = startRateAtTarget.wMulDown(adaptationMultiplier).bound( - AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET, AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET + ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET ); int256 endBorrowRate = _curve(endRateAtTarget, err); diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index bc83bd12..69740b84 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -217,7 +217,7 @@ contract AdaptativeCurveIrmTest is Test { } function testWExpWMulDownMaxRate() public pure { - MathLib.wExp(MathLib.WEXP_UPPER_BOUND).wMulDown(AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET); + MathLib.wExp(MathLib.WEXP_UPPER_BOUND).wMulDown(ConstantsLib.MAX_RATE_AT_TARGET); } /* HANDLERS */ @@ -244,12 +244,10 @@ contract AdaptativeCurveIrmTest is Test { market.totalSupplyAssets = 10 ether; assertGe( - irm.borrowRateView(marketParams, market), - uint256(AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) + irm.borrowRateView(marketParams, market), uint256(ConstantsLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) ); assertGe( - irm.borrowRate(marketParams, market), - uint256(AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) + irm.borrowRate(marketParams, market), uint256(ConstantsLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) ); } @@ -259,12 +257,10 @@ contract AdaptativeCurveIrmTest is Test { market.totalSupplyAssets = 10 ether; assertLe( - irm.borrowRateView(marketParams, market), - uint256(AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) + irm.borrowRateView(marketParams, market), uint256(ConstantsLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) ); assertLe( - irm.borrowRate(marketParams, market), - uint256(AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) + irm.borrowRate(marketParams, market), uint256(ConstantsLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) ); } @@ -276,7 +272,7 @@ contract AdaptativeCurveIrmTest is Test { int256 adaptationMultiplier = MathLib.wExp(linearAdaptation); return (rateAtTarget > 0) ? rateAtTarget.wMulDown(adaptationMultiplier).bound( - AdaptativeCurveIrmLib.MIN_RATE_AT_TARGET, AdaptativeCurveIrmLib.MAX_RATE_AT_TARGET + ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET ) : INITIAL_RATE_AT_TARGET; } From 614f4ff5950904555dc2bc0e70f90160474d87f6 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Sat, 11 Nov 2023 17:16:06 +0100 Subject: [PATCH 3/7] refactor(files): rename files --- ...peedJumpIrm.sol => AdaptativeCurveIrm.sol} | 5 +- src/libraries/MathLib.sol | 45 +------------- .../ConstantsLib.sol} | 2 +- src/libraries/adaptative-curve/ExpLib.sol | 49 +++++++++++++++ ...IrmTest.sol => AdaptativeCurveIrmTest.sol} | 7 +-- test/ExpLibTest.sol | 57 +++++++++++++++++ test/MathLibTest.sol | 62 ------------------- 7 files changed, 114 insertions(+), 113 deletions(-) rename src/{SpeedJumpIrm.sol => AdaptativeCurveIrm.sol} (97%) rename src/libraries/{AdaptativeCurveIrmLib.sol => adaptative-curve/ConstantsLib.sol} (95%) create mode 100644 src/libraries/adaptative-curve/ExpLib.sol rename test/{SpeedJumpIrmTest.sol => AdaptativeCurveIrmTest.sol} (98%) create mode 100644 test/ExpLibTest.sol delete mode 100644 test/MathLibTest.sol diff --git a/src/SpeedJumpIrm.sol b/src/AdaptativeCurveIrm.sol similarity index 97% rename from src/SpeedJumpIrm.sol rename to src/AdaptativeCurveIrm.sol index b77773fa..398e1421 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/AdaptativeCurveIrm.sol @@ -6,7 +6,8 @@ import {IIrm} from "../lib/morpho-blue/src/interfaces/IIrm.sol"; import {UtilsLib} from "./libraries/UtilsLib.sol"; import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {MathLib, WAD_INT as WAD} from "./libraries/MathLib.sol"; -import {AdaptativeCurveIrmLib as ConstantsLib} from "./libraries/AdaptativeCurveIrmLib.sol"; +import {ExpLib} from "./libraries/adaptative-curve/ExpLib.sol"; +import {ConstantsLib} from "./libraries/adaptative-curve/ConstantsLib.sol"; import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib.sol"; import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; @@ -135,7 +136,7 @@ contract AdaptativeCurveIrm is IIrm { // Safe "unchecked" cast because block.timestamp - market.lastUpdate <= block.timestamp <= type(int256).max. int256 elapsed = int256(block.timestamp - market.lastUpdate); int256 linearAdaptation = speed * elapsed; - int256 adaptationMultiplier = MathLib.wExp(linearAdaptation); + int256 adaptationMultiplier = ExpLib.wExp(linearAdaptation); // endRateAtTarget is bounded between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET. int256 endRateAtTarget = startRateAtTarget.wMulDown(adaptationMultiplier).bound( ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET diff --git a/src/libraries/MathLib.sol b/src/libraries/MathLib.sol index 120c7756..fb0c7000 100644 --- a/src/libraries/MathLib.sol +++ b/src/libraries/MathLib.sol @@ -8,51 +8,8 @@ int256 constant WAD_INT = int256(WAD); /// @title MathLib /// @author Morpho Labs /// @custom:contact security@morpho.org -/// @notice Library to manage fixed-point arithmetic and approximate the exponential function. +/// @notice Library to manage fixed-point arithmetic on signed integers. library MathLib { - using MathLib for uint128; - using MathLib for uint256; - using {wDivDown} for int256; - - /// @dev ln(2). - int256 internal constant LN_2_INT = 0.693147180559945309 ether; - - /// @dev ln(1e-18). - int256 internal constant LN_WEI_INT = -41.446531673892822312 ether; - - /// @dev Above this bound, `wExp` is clipped to avoid overflowing when multiplied with 1 ether. - /// @dev This upper bound corresponds to: ln(type(int256).max / 1e36) (scaled by WAD, floored). - int256 internal constant WEXP_UPPER_BOUND = 93.859467695000404319 ether; - - /// @dev The value of wExp(`WEXP_UPPER_BOUND`). - int256 internal constant WEXP_UPPER_VALUE = 57716089161558943862588783571184261698504.523000224082296832 ether; - - /// @dev Returns an approximation of exp. - function wExp(int256 x) internal pure returns (int256) { - unchecked { - // If x < ln(1e-18) then exp(x) < 1e-18 so it is rounded to zero. - if (x < LN_WEI_INT) return 0; - // `wExp` is clipped to avoid overflowing when multiplied with 1 ether. - if (x >= WEXP_UPPER_BOUND) return WEXP_UPPER_VALUE; - - // Decompose x as x = q * ln(2) + r with q an integer and -ln(2)/2 <= r <= ln(2)/2. - // q = x / ln(2) rounded half toward zero. - int256 roundingAdjustment = (x < 0) ? -(LN_2_INT / 2) : (LN_2_INT / 2); - // Safe unchecked because x is bounded. - int256 q = (x + roundingAdjustment) / LN_2_INT; - // Safe unchecked because |q * ln(2) - x| <= ln(2)/2. - int256 r = x - q * LN_2_INT; - - // Compute e^r with a 2nd-order Taylor polynomial. - // Safe unchecked because |r| < 1e18. - int256 expR = WAD_INT + r + (r * r) / WAD_INT / 2; - - // Return e^x = 2^q * e^r. - if (q >= 0) return expR << uint256(q); - else return expR >> uint256(-q); - } - } - function wMulDown(int256 a, int256 b) internal pure returns (int256) { return a * b / WAD_INT; } diff --git a/src/libraries/AdaptativeCurveIrmLib.sol b/src/libraries/adaptative-curve/ConstantsLib.sol similarity index 95% rename from src/libraries/AdaptativeCurveIrmLib.sol rename to src/libraries/adaptative-curve/ConstantsLib.sol index b492d9aa..7f715427 100644 --- a/src/libraries/AdaptativeCurveIrmLib.sol +++ b/src/libraries/adaptative-curve/ConstantsLib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -library AdaptativeCurveIrmLib { +library ConstantsLib { /// @notice Maximum rate at target per second (scaled by WAD) (1B% APR). int256 internal constant MAX_RATE_AT_TARGET = int256(0.01e9 ether) / 365 days; diff --git a/src/libraries/adaptative-curve/ExpLib.sol b/src/libraries/adaptative-curve/ExpLib.sol new file mode 100644 index 00000000..70381ed7 --- /dev/null +++ b/src/libraries/adaptative-curve/ExpLib.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {WAD_INT} from "../MathLib.sol"; + +/// @title ExpLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Library to approximate the exponential function. +library ExpLib { + /// @dev ln(2). + int256 internal constant LN_2_INT = 0.693147180559945309 ether; + + /// @dev ln(1e-18). + int256 internal constant LN_WEI_INT = -41.446531673892822312 ether; + + /// @dev Above this bound, `wExp` is clipped to avoid overflowing when multiplied with 1 ether. + /// @dev This upper bound corresponds to: ln(type(int256).max / 1e36) (scaled by WAD, floored). + int256 internal constant WEXP_UPPER_BOUND = 93.859467695000404319 ether; + + /// @dev The value of wExp(`WEXP_UPPER_BOUND`). + int256 internal constant WEXP_UPPER_VALUE = 57716089161558943862588783571184261698504.523000224082296832 ether; + + /// @dev Returns an approximation of exp. + function wExp(int256 x) internal pure returns (int256) { + unchecked { + // If x < ln(1e-18) then exp(x) < 1e-18 so it is rounded to zero. + if (x < LN_WEI_INT) return 0; + // `wExp` is clipped to avoid overflowing when multiplied with 1 ether. + if (x >= WEXP_UPPER_BOUND) return WEXP_UPPER_VALUE; + + // Decompose x as x = q * ln(2) + r with q an integer and -ln(2)/2 <= r <= ln(2)/2. + // q = x / ln(2) rounded half toward zero. + int256 roundingAdjustment = (x < 0) ? -(LN_2_INT / 2) : (LN_2_INT / 2); + // Safe unchecked because x is bounded. + int256 q = (x + roundingAdjustment) / LN_2_INT; + // Safe unchecked because |q * ln(2) - x| <= ln(2)/2. + int256 r = x - q * LN_2_INT; + + // Compute e^r with a 2nd-order Taylor polynomial. + // Safe unchecked because |r| < 1e18. + int256 expR = WAD_INT + r + (r * r) / WAD_INT / 2; + + // Return e^x = 2^q * e^r. + if (q >= 0) return expR << uint256(q); + else return expR >> uint256(-q); + } + } +} diff --git a/test/SpeedJumpIrmTest.sol b/test/AdaptativeCurveIrmTest.sol similarity index 98% rename from test/SpeedJumpIrmTest.sol rename to test/AdaptativeCurveIrmTest.sol index 69740b84..808e8073 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/AdaptativeCurveIrmTest.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "../src/SpeedJumpIrm.sol"; +import "../src/AdaptativeCurveIrm.sol"; import "../lib/forge-std/src/Test.sol"; contract AdaptativeCurveIrmTest is Test { - using MathLib for int256; using MathLib for int256; using MathLib for uint256; using UtilsLib for int256; @@ -217,7 +216,7 @@ contract AdaptativeCurveIrmTest is Test { } function testWExpWMulDownMaxRate() public pure { - MathLib.wExp(MathLib.WEXP_UPPER_BOUND).wMulDown(ConstantsLib.MAX_RATE_AT_TARGET); + ExpLib.wExp(ExpLib.WEXP_UPPER_BOUND).wMulDown(ConstantsLib.MAX_RATE_AT_TARGET); } /* HANDLERS */ @@ -269,7 +268,7 @@ contract AdaptativeCurveIrmTest is Test { int256 speed = ADJUSTMENT_SPEED.wMulDown(_err(market)); uint256 elapsed = (rateAtTarget > 0) ? block.timestamp - market.lastUpdate : 0; int256 linearAdaptation = speed * int256(elapsed); - int256 adaptationMultiplier = MathLib.wExp(linearAdaptation); + int256 adaptationMultiplier = ExpLib.wExp(linearAdaptation); return (rateAtTarget > 0) ? rateAtTarget.wMulDown(adaptationMultiplier).bound( ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET diff --git a/test/ExpLibTest.sol b/test/ExpLibTest.sol new file mode 100644 index 00000000..4497c409 --- /dev/null +++ b/test/ExpLibTest.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ExpLib} from "../src/libraries/adaptative-curve/ExpLib.sol"; +import {wadExp} from "../lib/solmate/src/utils/SignedWadMath.sol"; + +import "../lib/forge-std/src/Test.sol"; + +contract ExpLibTest is Test { + /// @dev ln(1e-9) truncated at 2 decimal places. + int256 internal constant LN_GWEI_INT = -20.72 ether; + + function testWExp(int256 x) public { + // Bounded to have sub-1% relative error. + x = bound(x, LN_GWEI_INT, ExpLib.WEXP_UPPER_BOUND); + + assertApproxEqRel(ExpLib.wExp(x), wadExp(x), 0.01 ether); + } + + function testWExpSmall(int256 x) public { + x = bound(x, ExpLib.LN_WEI_INT, LN_GWEI_INT); + + assertApproxEqAbs(ExpLib.wExp(x), 0, 1e10); + } + + function testWExpTooSmall(int256 x) public { + x = bound(x, type(int256).min, ExpLib.LN_WEI_INT); + + assertEq(ExpLib.wExp(x), 0); + } + + function testWExpTooLarge(int256 x) public { + x = bound(x, ExpLib.WEXP_UPPER_BOUND, type(int256).max); + + assertEq(ExpLib.wExp(x), ExpLib.WEXP_UPPER_VALUE); + } + + function testWExpDoesNotLeadToOverflow() public { + assertGt(ExpLib.WEXP_UPPER_VALUE * 1e18, 0); + } + + function testWExpContinuousUpperBound() public { + assertApproxEqRel(ExpLib.wExp(ExpLib.WEXP_UPPER_BOUND - 1), ExpLib.WEXP_UPPER_VALUE, 1e-10 ether); + } + + function testWExpPositive(int256 x) public { + x = bound(x, 0, type(int256).max); + + assertGe(ExpLib.wExp(x), 1e18); + } + + function testWExpNegative(int256 x) public { + x = bound(x, type(int256).min, 0); + + assertLe(ExpLib.wExp(x), 1e18); + } +} diff --git a/test/MathLibTest.sol b/test/MathLibTest.sol deleted file mode 100644 index 6ca864b5..00000000 --- a/test/MathLibTest.sol +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {MathLib} from "../src/libraries/MathLib.sol"; -import {ErrorsLib} from "../src/libraries/ErrorsLib.sol"; -import {wadExp} from "../lib/solmate/src/utils/SignedWadMath.sol"; - -import {AdaptativeCurveIrm} from "../src/SpeedJumpIrm.sol"; -import "../lib/forge-std/src/Test.sol"; - -contract MathLibTest is Test { - using MathLib for uint128; - using MathLib for uint256; - - /// @dev ln(1e-9) truncated at 2 decimal places. - int256 internal constant LN_GWEI_INT = -20.72 ether; - - function testWExp(int256 x) public { - // Bounded to have sub-1% relative error. - x = bound(x, LN_GWEI_INT, MathLib.WEXP_UPPER_BOUND); - - assertApproxEqRel(MathLib.wExp(x), wadExp(x), 0.01 ether); - } - - function testWExpSmall(int256 x) public { - x = bound(x, MathLib.LN_WEI_INT, LN_GWEI_INT); - - assertApproxEqAbs(MathLib.wExp(x), 0, 1e10); - } - - function testWExpTooSmall(int256 x) public { - x = bound(x, type(int256).min, MathLib.LN_WEI_INT); - - assertEq(MathLib.wExp(x), 0); - } - - function testWExpTooLarge(int256 x) public { - x = bound(x, MathLib.WEXP_UPPER_BOUND, type(int256).max); - - assertEq(MathLib.wExp(x), MathLib.WEXP_UPPER_VALUE); - } - - function testWExpDoesNotLeadToOverflow() public { - assertGt(MathLib.WEXP_UPPER_VALUE * 1e18, 0); - } - - function testWExpContinuousUpperBound() public { - assertApproxEqRel(MathLib.wExp(MathLib.WEXP_UPPER_BOUND - 1), MathLib.WEXP_UPPER_VALUE, 1e-10 ether); - } - - function testWExpPositive(int256 x) public { - x = bound(x, 0, type(int256).max); - - assertGe(MathLib.wExp(x), 1e18); - } - - function testWExpNegative(int256 x) public { - x = bound(x, type(int256).min, 0); - - assertLe(MathLib.wExp(x), 1e18); - } -} From c9a3e20000060fe4ad45ff6b67444cc2251fdca5 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Tue, 14 Nov 2023 11:46:31 +0100 Subject: [PATCH 4/7] test(irm): move exp lib test --- src/libraries/adaptative-curve/ExpLib.sol | 2 +- test/AdaptativeCurveIrmTest.sol | 4 --- test/ExpLibTest.sol | 31 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/libraries/adaptative-curve/ExpLib.sol b/src/libraries/adaptative-curve/ExpLib.sol index 70381ed7..e8c6f8ff 100644 --- a/src/libraries/adaptative-curve/ExpLib.sol +++ b/src/libraries/adaptative-curve/ExpLib.sol @@ -19,7 +19,7 @@ library ExpLib { int256 internal constant WEXP_UPPER_BOUND = 93.859467695000404319 ether; /// @dev The value of wExp(`WEXP_UPPER_BOUND`). - int256 internal constant WEXP_UPPER_VALUE = 57716089161558943862588783571184261698504.523000224082296832 ether; + int256 internal constant WEXP_UPPER_VALUE = 57716089161558943949701069502944508345128.422502756744429568 ether; /// @dev Returns an approximation of exp. function wExp(int256 x) internal pure returns (int256) { diff --git a/test/AdaptativeCurveIrmTest.sol b/test/AdaptativeCurveIrmTest.sol index 808e8073..336ef784 100644 --- a/test/AdaptativeCurveIrmTest.sol +++ b/test/AdaptativeCurveIrmTest.sol @@ -215,10 +215,6 @@ contract AdaptativeCurveIrmTest is Test { assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); } - function testWExpWMulDownMaxRate() public pure { - ExpLib.wExp(ExpLib.WEXP_UPPER_BOUND).wMulDown(ConstantsLib.MAX_RATE_AT_TARGET); - } - /* HANDLERS */ function handleBorrowRate(uint256 totalSupplyAssets, uint256 totalBorrowAssets, uint256 elapsed) external { diff --git a/test/ExpLibTest.sol b/test/ExpLibTest.sol index 4497c409..77bc9e3b 100644 --- a/test/ExpLibTest.sol +++ b/test/ExpLibTest.sol @@ -1,12 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; +import {MathLib, WAD_INT} from "../src/libraries/MathLib.sol"; +import {ConstantsLib} from "../src/libraries/adaptative-curve/ConstantsLib.sol"; import {ExpLib} from "../src/libraries/adaptative-curve/ExpLib.sol"; import {wadExp} from "../lib/solmate/src/utils/SignedWadMath.sol"; +import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; import "../lib/forge-std/src/Test.sol"; contract ExpLibTest is Test { + using MathLib for int256; + using MorphoMathLib for uint256; + /// @dev ln(1e-9) truncated at 2 decimal places. int256 internal constant LN_GWEI_INT = -20.72 ether; @@ -41,6 +47,7 @@ contract ExpLibTest is Test { function testWExpContinuousUpperBound() public { assertApproxEqRel(ExpLib.wExp(ExpLib.WEXP_UPPER_BOUND - 1), ExpLib.WEXP_UPPER_VALUE, 1e-10 ether); + assertEq(_wExpUnbounded(ExpLib.WEXP_UPPER_BOUND), ExpLib.WEXP_UPPER_VALUE); } function testWExpPositive(int256 x) public { @@ -54,4 +61,28 @@ contract ExpLibTest is Test { assertLe(ExpLib.wExp(x), 1e18); } + + function testWExpWMulDownMaxRate() public pure { + ExpLib.wExp(ExpLib.WEXP_UPPER_BOUND).wMulDown(ConstantsLib.MAX_RATE_AT_TARGET); + } + + function _wExpUnbounded(int256 x) internal pure returns (int256) { + unchecked { + // Decompose x as x = q * ln(2) + r with q an integer and -ln(2)/2 <= r <= ln(2)/2. + // q = x / ln(2) rounded half toward zero. + int256 roundingAdjustment = (x < 0) ? -(ExpLib.LN_2_INT / 2) : (ExpLib.LN_2_INT / 2); + // Safe unchecked because x is bounded. + int256 q = (x + roundingAdjustment) / ExpLib.LN_2_INT; + // Safe unchecked because |q * ln(2) - x| <= ln(2)/2. + int256 r = x - q * ExpLib.LN_2_INT; + + // Compute e^r with a 2nd-order Taylor polynomial. + // Safe unchecked because |r| < 1e18. + int256 expR = WAD_INT + r + (r * r) / WAD_INT / 2; + + // Return e^x = 2^q * e^r. + if (q >= 0) return expR << uint256(q); + else return expR >> uint256(-q); + } + } } From ae8ab3c01fa9eb2a6405a5968e1cdde2c476a36c Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 16 Nov 2023 16:10:02 +0100 Subject: [PATCH 5/7] build(lib): update morpho-blue --- lib/morpho-blue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/morpho-blue b/lib/morpho-blue index 5e515a60..f463e40f 160000 --- a/lib/morpho-blue +++ b/lib/morpho-blue @@ -1 +1 @@ -Subproject commit 5e515a607a24dcaa04ebf4dcce8e49419353e5aa +Subproject commit f463e40f776acd0f26d0d380b51cfd02949c8c23 From 0f7774c7cd433ffd30e74d13b1014c370cc6b41f Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 16 Nov 2023 19:36:43 +0100 Subject: [PATCH 6/7] refactor(files): rename to adaptive --- src/{AdaptativeCurveIrm.sol => AdaptiveCurveIrm.sol} | 4 ++-- .../{adaptative-curve => adaptive-curve}/ConstantsLib.sol | 0 src/libraries/{adaptative-curve => adaptive-curve}/ExpLib.sol | 0 test/{AdaptativeCurveIrmTest.sol => AdaptiveCurveIrmTest.sol} | 2 +- test/ExpLibTest.sol | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{AdaptativeCurveIrm.sol => AdaptiveCurveIrm.sol} (98%) rename src/libraries/{adaptative-curve => adaptive-curve}/ConstantsLib.sol (100%) rename src/libraries/{adaptative-curve => adaptive-curve}/ExpLib.sol (100%) rename test/{AdaptativeCurveIrmTest.sol => AdaptiveCurveIrmTest.sol} (99%) diff --git a/src/AdaptativeCurveIrm.sol b/src/AdaptiveCurveIrm.sol similarity index 98% rename from src/AdaptativeCurveIrm.sol rename to src/AdaptiveCurveIrm.sol index cc63659b..9785dad4 100644 --- a/src/AdaptativeCurveIrm.sol +++ b/src/AdaptiveCurveIrm.sol @@ -6,8 +6,8 @@ import {IIrm} from "../lib/morpho-blue/src/interfaces/IIrm.sol"; import {UtilsLib} from "./libraries/UtilsLib.sol"; import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {MathLib, WAD_INT as WAD} from "./libraries/MathLib.sol"; -import {ExpLib} from "./libraries/adaptative-curve/ExpLib.sol"; -import {ConstantsLib} from "./libraries/adaptative-curve/ConstantsLib.sol"; +import {ExpLib} from "./libraries/adaptive-curve/ExpLib.sol"; +import {ConstantsLib} from "./libraries/adaptive-curve/ConstantsLib.sol"; import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib.sol"; import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; diff --git a/src/libraries/adaptative-curve/ConstantsLib.sol b/src/libraries/adaptive-curve/ConstantsLib.sol similarity index 100% rename from src/libraries/adaptative-curve/ConstantsLib.sol rename to src/libraries/adaptive-curve/ConstantsLib.sol diff --git a/src/libraries/adaptative-curve/ExpLib.sol b/src/libraries/adaptive-curve/ExpLib.sol similarity index 100% rename from src/libraries/adaptative-curve/ExpLib.sol rename to src/libraries/adaptive-curve/ExpLib.sol diff --git a/test/AdaptativeCurveIrmTest.sol b/test/AdaptiveCurveIrmTest.sol similarity index 99% rename from test/AdaptativeCurveIrmTest.sol rename to test/AdaptiveCurveIrmTest.sol index 257056e5..86fbe3e3 100644 --- a/test/AdaptativeCurveIrmTest.sol +++ b/test/AdaptiveCurveIrmTest.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "../src/AdaptativeCurveIrm.sol"; +import "../src/AdaptiveCurveIrm.sol"; import "../lib/forge-std/src/Test.sol"; diff --git a/test/ExpLibTest.sol b/test/ExpLibTest.sol index 77bc9e3b..35280eb6 100644 --- a/test/ExpLibTest.sol +++ b/test/ExpLibTest.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.0; import {MathLib, WAD_INT} from "../src/libraries/MathLib.sol"; -import {ConstantsLib} from "../src/libraries/adaptative-curve/ConstantsLib.sol"; -import {ExpLib} from "../src/libraries/adaptative-curve/ExpLib.sol"; +import {ConstantsLib} from "../src/libraries/adaptive-curve/ConstantsLib.sol"; +import {ExpLib} from "../src/libraries/adaptive-curve/ExpLib.sol"; import {wadExp} from "../lib/solmate/src/utils/SignedWadMath.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; From 2b9ad3741d3eb5e68a3a18e86c2ca7c740982029 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Thu, 16 Nov 2023 19:41:08 +0100 Subject: [PATCH 7/7] docs(file): add contract natspecs --- src/libraries/adaptive-curve/ConstantsLib.sol | 5 ++++- src/libraries/adaptive-curve/ExpLib.sol | 2 +- test/ExpLibTest.sol | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libraries/adaptive-curve/ConstantsLib.sol b/src/libraries/adaptive-curve/ConstantsLib.sol index 7f715427..3166d1c4 100644 --- a/src/libraries/adaptive-curve/ConstantsLib.sol +++ b/src/libraries/adaptive-curve/ConstantsLib.sol @@ -1,6 +1,9 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +/// @title ConstantsLib +/// @author Morpho Labs +/// @custom:contact security@morpho.org library ConstantsLib { /// @notice Maximum rate at target per second (scaled by WAD) (1B% APR). int256 internal constant MAX_RATE_AT_TARGET = int256(0.01e9 ether) / 365 days; diff --git a/src/libraries/adaptive-curve/ExpLib.sol b/src/libraries/adaptive-curve/ExpLib.sol index e8c6f8ff..27110d31 100644 --- a/src/libraries/adaptive-curve/ExpLib.sol +++ b/src/libraries/adaptive-curve/ExpLib.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {WAD_INT} from "../MathLib.sol"; diff --git a/test/ExpLibTest.sol b/test/ExpLibTest.sol index 35280eb6..81e4cd5d 100644 --- a/test/ExpLibTest.sol +++ b/test/ExpLibTest.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {MathLib, WAD_INT} from "../src/libraries/MathLib.sol";