diff --git a/foundry.toml b/foundry.toml index 8318e07d..f3334ec6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,7 @@ src = "src" out = "out" test = "test" libs = ["lib"] +optimizer_runs = 999999 # Etherscan does not support verifying contracts with more optimization runs. [profile.default.fuzz] runs = 4096 diff --git a/lib/morpho-blue b/lib/morpho-blue index f463e40f..55d2d993 160000 --- a/lib/morpho-blue +++ b/lib/morpho-blue @@ -1 +1 @@ -Subproject commit f463e40f776acd0f26d0d380b51cfd02949c8c23 +Subproject commit 55d2d99304fb3fb930c688462ae2ccabb1d533ad diff --git a/src/AdaptiveCurveIrm.sol b/src/AdaptiveCurveIrm.sol index 9785dad4..f19ec822 100644 --- a/src/AdaptiveCurveIrm.sol +++ b/src/AdaptiveCurveIrm.sol @@ -2,11 +2,12 @@ pragma solidity 0.8.19; import {IIrm} from "../lib/morpho-blue/src/interfaces/IIrm.sol"; +import {IAdaptiveCurveIrm} from "./interfaces/IAdaptiveCurveIrm.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/adaptive-curve/ExpLib.sol"; +import {MathLib, WAD_INT as WAD} from "./libraries/MathLib.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"; @@ -15,11 +16,10 @@ import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib /// @title AdaptiveCurveIrm /// @author Morpho Labs /// @custom:contact security@morpho.org -contract AdaptiveCurveIrm is IIrm { +contract AdaptiveCurveIrm is IAdaptiveCurveIrm { using MathLib for int256; using UtilsLib for int256; using MorphoMathLib for uint128; - using MorphoMathLib for uint256; using MarketParamsLib for MarketParams; /* EVENTS */ @@ -29,63 +29,22 @@ contract AdaptiveCurveIrm is IIrm { /* IMMUTABLES */ - /// @notice Address of Morpho. + /// @inheritdoc IAdaptiveCurveIrm 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; - /* STORAGE */ - /// @notice Rate at target utilization. - /// @dev Tells the height of the curve. + /// @inheritdoc IAdaptiveCurveIrm mapping(Id => int256) public rateAtTarget; /* CONSTRUCTOR */ /// @notice Constructor. /// @param morpho The address of Morpho. - /// @param curveSteepness The curve steepness (scaled by WAD). - /// @param adjustmentSpeed The adjustment speed (scaled by WAD). - /// @param targetUtilization The target utilization (scaled by WAD). - /// @param initialRateAtTarget The initial rate at target (scaled by WAD). - constructor( - address morpho, - int256 curveSteepness, - int256 adjustmentSpeed, - int256 targetUtilization, - int256 initialRateAtTarget - ) { + constructor(address morpho) { require(morpho != address(0), ErrorsLib.ZERO_ADDRESS); - require(curveSteepness >= WAD, ErrorsLib.INPUT_TOO_SMALL); - require(curveSteepness <= ConstantsLib.MAX_CURVE_STEEPNESS, ErrorsLib.INPUT_TOO_LARGE); - require(adjustmentSpeed >= 0, ErrorsLib.INPUT_TOO_SMALL); - 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 >= 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; - ADJUSTMENT_SPEED = adjustmentSpeed; - TARGET_UTILIZATION = targetUtilization; - INITIAL_RATE_AT_TARGET = initialRateAtTarget; } /* BORROW RATES */ @@ -119,8 +78,10 @@ contract AdaptiveCurveIrm is IIrm { int256 utilization = int256(market.totalSupplyAssets > 0 ? market.totalBorrowAssets.wDivDown(market.totalSupplyAssets) : 0); - int256 errNormFactor = utilization > TARGET_UTILIZATION ? WAD - TARGET_UTILIZATION : TARGET_UTILIZATION; - int256 err = (utilization - TARGET_UTILIZATION).wDivDown(errNormFactor); + int256 errNormFactor = utilization > ConstantsLib.TARGET_UTILIZATION + ? WAD - ConstantsLib.TARGET_UTILIZATION + : ConstantsLib.TARGET_UTILIZATION; + int256 err = (utilization - ConstantsLib.TARGET_UTILIZATION).wDivToZero(errNormFactor); int256 startRateAtTarget = rateAtTarget[id]; @@ -129,12 +90,12 @@ contract AdaptiveCurveIrm is IIrm { if (startRateAtTarget == 0) { // First interaction. - avgRateAtTarget = INITIAL_RATE_AT_TARGET; - endRateAtTarget = INITIAL_RATE_AT_TARGET; + avgRateAtTarget = ConstantsLib.INITIAL_RATE_AT_TARGET; + endRateAtTarget = ConstantsLib.INITIAL_RATE_AT_TARGET; } else { - // Note that the speed is assumed constant between two interactions, but in theory it increases because of - // interests. So the rate will be slightly underestimated. - int256 speed = ADJUSTMENT_SPEED.wMulDown(err); + // The speed is assumed constant between two updates, but it is in fact not constant because of interest. + // So the rate is always underestimated. + int256 speed = ConstantsLib.ADJUSTMENT_SPEED.wMulToZero(err); // market.lastUpdate != 0 because it is not the first interaction with this market. // Safe "unchecked" cast because block.timestamp - market.lastUpdate <= block.timestamp <= type(int256).max. int256 elapsed = int256(block.timestamp - market.lastUpdate); @@ -172,18 +133,18 @@ contract AdaptiveCurveIrm is IIrm { /// The formula of the curve is the following: /// r = ((1-1/C)*err + 1) * rateAtTarget if err < 0 /// ((C-1)*err + 1) * rateAtTarget else. - function _curve(int256 _rateAtTarget, int256 err) private view returns (int256) { + function _curve(int256 _rateAtTarget, int256 err) private pure returns (int256) { // Non negative because 1 - 1/C >= 0, C - 1 >= 0. - int256 coeff = err < 0 ? WAD - WAD.wDivDown(CURVE_STEEPNESS) : CURVE_STEEPNESS - WAD; + int256 coeff = err < 0 ? WAD - WAD.wDivToZero(ConstantsLib.CURVE_STEEPNESS) : ConstantsLib.CURVE_STEEPNESS - WAD; // Non negative if _rateAtTarget >= 0 because if err < 0, coeff <= 1. - return (coeff.wMulDown(err) + WAD).wMulDown(int256(_rateAtTarget)); + return (coeff.wMulToZero(err) + WAD).wMulToZero(int256(_rateAtTarget)); } /// @dev Returns the new rate at target, for a given `startRateAtTarget` and a given `linearAdaptation`. /// The formula is: max(min(startRateAtTarget * exp(linearAdaptation), maxRateAtTarget), minRateAtTarget). function _newRateAtTarget(int256 startRateAtTarget, int256 linearAdaptation) private pure returns (int256) { // Non negative because MIN_RATE_AT_TARGET > 0. - return startRateAtTarget.wMulDown(ExpLib.wExp(linearAdaptation)).bound( + return startRateAtTarget.wMulToZero(ExpLib.wExp(linearAdaptation)).bound( ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET ); } diff --git a/src/interfaces/IAdaptiveCurveIrm.sol b/src/interfaces/IAdaptiveCurveIrm.sol new file mode 100644 index 00000000..0f2659a4 --- /dev/null +++ b/src/interfaces/IAdaptiveCurveIrm.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.0; + +import {IIrm} from "../../lib/morpho-blue/src/interfaces/IIrm.sol"; +import {Id} from "../../lib/morpho-blue/src/interfaces/IMorpho.sol"; + +/// @title IAdaptiveCurveIrm +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Interface exposed by the AdaptiveCurveIrm. +interface IAdaptiveCurveIrm is IIrm { + /// @notice Address of Morpho. + function MORPHO() external view returns (address); + + /// @notice Rate at target utilization. + /// @dev Tells the height of the curve. + function rateAtTarget(Id id) external view returns (int256); +} diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 8a88f349..5cb2c632 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -6,18 +6,9 @@ pragma solidity ^0.8.0; /// @custom:contact security@morpho.org /// @notice Library exposing error messages. library ErrorsLib { - /// @dev Thrown when the input is too large to fit in the expected type. - string internal constant INPUT_TOO_LARGE = "input too large"; - - /// @dev Thrown when the input is too small. - string internal constant INPUT_TOO_SMALL = "input too small"; - /// @dev Thrown when passing the zero address. string internal constant ZERO_ADDRESS = "zero address"; - /// @dev Thrown when passing the zero input. - string internal constant ZERO_INPUT = "zero input"; - /// @dev Thrown when the caller is not Morpho. string internal constant NOT_MORPHO = "not Morpho"; } diff --git a/src/libraries/MathLib.sol b/src/libraries/MathLib.sol index fb0c7000..f9cfe3d9 100644 --- a/src/libraries/MathLib.sol +++ b/src/libraries/MathLib.sol @@ -10,11 +10,13 @@ int256 constant WAD_INT = int256(WAD); /// @custom:contact security@morpho.org /// @notice Library to manage fixed-point arithmetic on signed integers. library MathLib { - function wMulDown(int256 a, int256 b) internal pure returns (int256) { - return a * b / WAD_INT; + /// @dev Returns the multiplication of `x` by `y` (in WAD) rounded towards 0. + function wMulToZero(int256 x, int256 y) internal pure returns (int256) { + return (x * y) / WAD_INT; } - function wDivDown(int256 a, int256 b) internal pure returns (int256) { - return a * WAD_INT / b; + /// @dev Returns the division of `x` by `y` (in WAD) rounded towards 0. + function wDivToZero(int256 x, int256 y) internal pure returns (int256) { + return (x * WAD_INT) / y; } } diff --git a/src/libraries/adaptive-curve/ConstantsLib.sol b/src/libraries/adaptive-curve/ConstantsLib.sol index 3166d1c4..a2c09ad8 100644 --- a/src/libraries/adaptive-curve/ConstantsLib.sol +++ b/src/libraries/adaptive-curve/ConstantsLib.sol @@ -5,15 +5,29 @@ pragma solidity ^0.8.0; /// @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; + /// @notice Curve steepness (scaled by WAD). + /// @dev Curve steepness = 4. + int256 public constant CURVE_STEEPNESS = 4 ether; - /// @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 Adjustment speed per second (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). + /// @dev Adjustment speed = 50/year. + int256 public constant ADJUSTMENT_SPEED = 50 ether / int256(365 days); - /// @notice Maximum curve steepness allowed (scaled by WAD). - int256 internal constant MAX_CURVE_STEEPNESS = 100 ether; + /// @notice Target utilization (scaled by WAD). + /// @dev Target utilization = 90%. + int256 public constant TARGET_UTILIZATION = 0.9 ether; - /// @notice Maximum adjustment speed allowed (scaled by WAD). - int256 internal constant MAX_ADJUSTMENT_SPEED = int256(1_000 ether) / 365 days; + /// @notice Initial rate at target per second (scaled by WAD). + /// @dev Initial rate at target = 4% (rate between 1% and 16%). + int256 public constant INITIAL_RATE_AT_TARGET = 0.04 ether / int256(365 days); + + /// @notice Minimum rate at target per second (scaled by WAD). + /// @dev Minimum rate at target = 0.1% (minimum rate = 0.025%). + int256 public constant MIN_RATE_AT_TARGET = 0.001 ether / int256(365 days); + + /// @notice Maximum rate at target per second (scaled by WAD). + /// @dev Maximum rate at target = 200% (maximum rate = 800%). + int256 public constant MAX_RATE_AT_TARGET = 2.0 ether / int256(365 days); } diff --git a/test/forge/AdaptiveCurveIrmTest.sol b/test/forge/AdaptiveCurveIrmTest.sol index 7432d62e..8d821340 100644 --- a/test/forge/AdaptiveCurveIrmTest.sol +++ b/test/forge/AdaptiveCurveIrmTest.sol @@ -15,18 +15,11 @@ contract AdaptiveCurveIrmTest is Test { event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 rateAtTarget); - int256 internal constant CURVE_STEEPNESS = 4 ether; - int256 internal constant ADJUSTMENT_SPEED = int256(50 ether) / 365 days; - int256 internal constant TARGET_UTILIZATION = 0.9 ether; - int256 internal constant INITIAL_RATE_AT_TARGET = int256(0.01 ether) / 365 days; - - AdaptiveCurveIrm internal irm; + IAdaptiveCurveIrm internal irm; MarketParams internal marketParams = MarketParams(address(0), address(0), address(0), address(0), 0); function setUp() public { - irm = new AdaptiveCurveIrm( - address(this), CURVE_STEEPNESS, ADJUSTMENT_SPEED, TARGET_UTILIZATION, INITIAL_RATE_AT_TARGET - ); + irm = new AdaptiveCurveIrm(address(this)); vm.warp(90 days); bytes4[] memory selectors = new bytes4[](1); @@ -39,16 +32,19 @@ contract AdaptiveCurveIrmTest is Test { function testDeployment() public { vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - new AdaptiveCurveIrm(address(0), 0, 0, 0, 0); + new AdaptiveCurveIrm(address(0)); } function testFirstBorrowRateUtilizationZero() public { Market memory market; assertApproxEqRel( - irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET / 4), 0.0001 ether, "avgBorrowRate" + irm.borrowRate(marketParams, market), + uint256(ConstantsLib.INITIAL_RATE_AT_TARGET / 4), + 0.0001 ether, + "avgBorrowRate" ); - assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET, "rateAtTarget"); + assertEq(irm.rateAtTarget(marketParams.id()), ConstantsLib.INITIAL_RATE_AT_TARGET, "rateAtTarget"); } function testFirstBorrowRateUtilizationOne() public { @@ -56,14 +52,18 @@ contract AdaptiveCurveIrmTest is Test { market.totalBorrowAssets = 1 ether; market.totalSupplyAssets = 1 ether; - assertEq(irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET * 4), "avgBorrowRate"); - assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET, "rateAtTarget"); + assertEq( + irm.borrowRate(marketParams, market), uint256(ConstantsLib.INITIAL_RATE_AT_TARGET * 4), "avgBorrowRate" + ); + assertEq(irm.rateAtTarget(marketParams.id()), ConstantsLib.INITIAL_RATE_AT_TARGET, "rateAtTarget"); } function testRateAfterUtilizationOne() public { vm.warp(365 days * 2); Market memory market; - assertApproxEqRel(irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET / 4), 0.001 ether); + assertApproxEqRel( + irm.borrowRate(marketParams, market), uint256(ConstantsLib.INITIAL_RATE_AT_TARGET / 4), 0.001 ether + ); market.totalBorrowAssets = 1 ether; market.totalSupplyAssets = 1 ether; @@ -72,23 +72,29 @@ contract AdaptiveCurveIrmTest is Test { // (exp((50/365)*5) ~= 1.9836. assertApproxEqRel( irm.borrowRateView(marketParams, market), - uint256((INITIAL_RATE_AT_TARGET * 4).wMulDown((1.9836 ether - 1 ether) * WAD / (ADJUSTMENT_SPEED * 5 days))), + uint256( + (ConstantsLib.INITIAL_RATE_AT_TARGET * 4).wMulToZero( + (1.9836 ether - 1 ether) * WAD / (ConstantsLib.ADJUSTMENT_SPEED * 5 days) + ) + ), 0.1 ether ); // The average value of exp((50/365)*x) between 0 and 5 is approx. 1.4361. assertApproxEqRel( irm.borrowRateView(marketParams, market), - uint256((INITIAL_RATE_AT_TARGET * 4).wMulDown(1.4361 ether)), + uint256((ConstantsLib.INITIAL_RATE_AT_TARGET * 4).wMulToZero(1.4361 ether)), 0.1 ether ); - // Expected rate: 5.744%. - assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.05744 ether) / 365 days, 0.1 ether); + // Expected rate: 22.976%. + assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.22976 ether) / 365 days, 0.1 ether); } function testRateAfterUtilizationZero() public { vm.warp(365 days * 2); Market memory market; - assertApproxEqRel(irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET / 4), 0.001 ether); + assertApproxEqRel( + irm.borrowRate(marketParams, market), uint256(ConstantsLib.INITIAL_RATE_AT_TARGET / 4), 0.001 ether + ); market.totalBorrowAssets = 0 ether; market.totalSupplyAssets = 1 ether; @@ -98,45 +104,47 @@ contract AdaptiveCurveIrmTest is Test { assertApproxEqRel( irm.borrowRateView(marketParams, market), uint256( - (INITIAL_RATE_AT_TARGET / 4).wMulDown((0.5041 ether - 1 ether) * WAD / (-ADJUSTMENT_SPEED * 5 days)) + (ConstantsLib.INITIAL_RATE_AT_TARGET / 4).wMulToZero( + (0.5041 ether - 1 ether) * WAD / (-ConstantsLib.ADJUSTMENT_SPEED * 5 days) + ) ), 0.1 ether ); // The average value of exp((-50/365*x)) between 0 and 5 is approx. 0.7240. assertApproxEqRel( irm.borrowRateView(marketParams, market), - uint256((INITIAL_RATE_AT_TARGET / 4).wMulDown(0.724 ether)), + uint256((ConstantsLib.INITIAL_RATE_AT_TARGET / 4).wMulToZero(0.724 ether)), 0.1 ether ); - // Expected rate: 0.181%. - assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.00181 ether) / 365 days, 0.1 ether); + // Expected rate: 0.7240%. + assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.00724 ether) / 365 days, 0.1 ether); } function testRateAfter45DaysUtilizationAboveTargetNoPing() public { Market memory market; market.totalSupplyAssets = 1 ether; - market.totalBorrowAssets = uint128(uint256(TARGET_UTILIZATION)); - assertEq(irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET)); - assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET); + market.totalBorrowAssets = uint128(uint256(ConstantsLib.TARGET_UTILIZATION)); + assertEq(irm.borrowRate(marketParams, market), uint256(ConstantsLib.INITIAL_RATE_AT_TARGET)); + assertEq(irm.rateAtTarget(marketParams.id()), ConstantsLib.INITIAL_RATE_AT_TARGET); market.lastUpdate = uint128(block.timestamp); vm.warp(block.timestamp + 45 days); - market.totalBorrowAssets = uint128(uint256(TARGET_UTILIZATION + 1 ether) / 2); // Error = 50% + market.totalBorrowAssets = uint128(uint256(ConstantsLib.TARGET_UTILIZATION + 1 ether) / 2); // Error = 50% irm.borrowRate(marketParams, market); - // Expected rate: 1% * exp(50 * 45 / 365 * 50%) = 21.81%. - assertApproxEqRel(irm.rateAtTarget(marketParams.id()), int256(0.2181 ether) / 365 days, 0.005 ether); + // Expected rate: 4% * exp(50 * 45 / 365 * 50%) = 87.22%. + assertApproxEqRel(irm.rateAtTarget(marketParams.id()), int256(0.8722 ether) / 365 days, 0.005 ether); } function testRateAfter45DaysUtilizationAboveTargetPingEveryMinute() public { Market memory market; market.totalSupplyAssets = 1 ether; - market.totalBorrowAssets = uint128(uint256(TARGET_UTILIZATION)); - assertEq(irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET)); - assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET); + market.totalBorrowAssets = uint128(uint256(ConstantsLib.TARGET_UTILIZATION)); + assertEq(irm.borrowRate(marketParams, market), uint256(ConstantsLib.INITIAL_RATE_AT_TARGET)); + assertEq(irm.rateAtTarget(marketParams.id()), ConstantsLib.INITIAL_RATE_AT_TARGET); - uint128 initialBorrowAssets = uint128(uint256(TARGET_UTILIZATION + 1 ether) / 2); // Error = 50% + uint128 initialBorrowAssets = uint128(uint256(ConstantsLib.TARGET_UTILIZATION + 1 ether) / 2); // Error = 50% market.totalBorrowAssets = initialBorrowAssets; @@ -151,20 +159,20 @@ contract AdaptiveCurveIrmTest is Test { } assertApproxEqRel( - market.totalBorrowAssets.wDivDown(market.totalSupplyAssets), 0.95 ether, 0.002 ether, "utilization" + market.totalBorrowAssets.wDivDown(market.totalSupplyAssets), 0.95 ether, 0.01 ether, "utilization" ); int256 rateAtTarget = irm.rateAtTarget(marketParams.id()); - // Expected rate: 1% * exp(50 * 45 / 365 * 50%) = 21.81%. - int256 expectedRateAtTarget = int256(0.2181 ether) / 365 days; + // Expected rate: 4% * exp(50 * 45 / 365 * 50%) = 87.22%. + int256 expectedRateAtTarget = int256(0.8722 ether) / 365 days; assertGe(rateAtTarget, expectedRateAtTarget); - // The rate is tolerated to be +2% (relatively) because of the pings every minute. - assertApproxEqRel(rateAtTarget, expectedRateAtTarget, 0.02 ether, "expectedRateAtTarget"); + // The rate is tolerated to be +8% (relatively) because of the pings every minute. + assertApproxEqRel(rateAtTarget, expectedRateAtTarget, 0.08 ether, "expectedRateAtTarget"); - // Expected growth: exp(21.81% * 3.5 * 45 / 365) = +9.87%. - // The growth is tolerated to be +8% (relatively) because of the pings every minute. + // Expected growth: exp(87.22% * 3.5 * 45 / 365) = +45.70%. + // The growth is tolerated to be +30% (relatively) because of the pings every minute. assertApproxEqRel( - market.totalBorrowAssets, initialBorrowAssets.wMulDown(1.0987 ether), 0.08 ether, "totalBorrowAssets" + market.totalBorrowAssets, initialBorrowAssets.wMulDown(1.457 ether), 0.3 ether, "totalBorrowAssets" ); } @@ -173,30 +181,26 @@ contract AdaptiveCurveIrmTest is Test { Market memory market; market.totalSupplyAssets = 1 ether; - market.totalBorrowAssets = uint128(uint256(TARGET_UTILIZATION)); - assertEq(irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET)); - assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET); + market.totalBorrowAssets = uint128(uint256(ConstantsLib.TARGET_UTILIZATION)); + assertEq(irm.borrowRate(marketParams, market), uint256(ConstantsLib.INITIAL_RATE_AT_TARGET)); + assertEq(irm.rateAtTarget(marketParams.id()), ConstantsLib.INITIAL_RATE_AT_TARGET); market.lastUpdate = uint128(block.timestamp); vm.warp(block.timestamp + elapsed); irm.borrowRate(marketParams, market); - assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET); + assertEq(irm.rateAtTarget(marketParams.id()), ConstantsLib.INITIAL_RATE_AT_TARGET); } function testRateAfter3WeeksUtilizationTargetPingEveryMinute() public { - int256 initialRateAtTarget = int256(1 ether) / 365 days; // 100% - - irm = new AdaptiveCurveIrm( - address(this), CURVE_STEEPNESS, ADJUSTMENT_SPEED, TARGET_UTILIZATION, initialRateAtTarget - ); + irm = new AdaptiveCurveIrm(address(this)); Market memory market; market.totalSupplyAssets = 1 ether; - market.totalBorrowAssets = uint128(uint256(TARGET_UTILIZATION)); - assertEq(irm.borrowRate(marketParams, market), uint256(initialRateAtTarget)); - assertEq(irm.rateAtTarget(marketParams.id()), initialRateAtTarget); + market.totalBorrowAssets = uint128(uint256(ConstantsLib.TARGET_UTILIZATION)); + assertEq(irm.borrowRate(marketParams, market), uint256(ConstantsLib.INITIAL_RATE_AT_TARGET)); + assertEq(irm.rateAtTarget(marketParams.id()), ConstantsLib.INITIAL_RATE_AT_TARGET); for (uint256 i; i < 3 weeks / 1 minutes; ++i) { market.lastUpdate = uint128(block.timestamp); @@ -209,13 +213,15 @@ contract AdaptiveCurveIrmTest is Test { } assertApproxEqRel( - market.totalBorrowAssets.wDivDown(market.totalSupplyAssets), uint256(TARGET_UTILIZATION), 0.01 ether + market.totalBorrowAssets.wDivDown(market.totalSupplyAssets), + uint256(ConstantsLib.TARGET_UTILIZATION), + 0.01 ether ); int256 rateAtTarget = irm.rateAtTarget(marketParams.id()); - assertGe(rateAtTarget, initialRateAtTarget); + assertGe(rateAtTarget, ConstantsLib.INITIAL_RATE_AT_TARGET); // The rate is tolerated to be +10% (relatively) because of the pings every minute. - assertApproxEqRel(rateAtTarget, initialRateAtTarget, 0.1 ether); + assertApproxEqRel(rateAtTarget, ConstantsLib.INITIAL_RATE_AT_TARGET, 0.1 ether); } function testFirstBorrowRate(Market memory market) public { @@ -225,8 +231,8 @@ contract AdaptiveCurveIrmTest is Test { uint256 avgBorrowRate = irm.borrowRate(marketParams, market); int256 rateAtTarget = irm.rateAtTarget(marketParams.id()); - assertEq(avgBorrowRate, _curve(int256(INITIAL_RATE_AT_TARGET), _err(market)), "avgBorrowRate"); - assertEq(rateAtTarget, INITIAL_RATE_AT_TARGET, "rateAtTarget"); + assertEq(avgBorrowRate, _curve(int256(ConstantsLib.INITIAL_RATE_AT_TARGET), _err(market)), "avgBorrowRate"); + assertEq(rateAtTarget, ConstantsLib.INITIAL_RATE_AT_TARGET, "rateAtTarget"); } function testBorrowRateEventEmission(Market memory market) public { @@ -236,7 +242,7 @@ contract AdaptiveCurveIrmTest is Test { vm.expectEmit(true, true, true, true, address(irm)); emit BorrowRateUpdate( marketParams.id(), - _curve(int256(INITIAL_RATE_AT_TARGET), _err(market)), + _curve(int256(ConstantsLib.INITIAL_RATE_AT_TARGET), _err(market)), uint256(_expectedRateAtTarget(marketParams.id(), market)) ); irm.borrowRate(marketParams, market); @@ -249,7 +255,7 @@ contract AdaptiveCurveIrmTest is Test { uint256 avgBorrowRate = irm.borrowRateView(marketParams, market); int256 rateAtTarget = irm.rateAtTarget(marketParams.id()); - assertEq(avgBorrowRate, _curve(int256(INITIAL_RATE_AT_TARGET), _err(market)), "avgBorrowRate"); + assertEq(avgBorrowRate, _curve(int256(ConstantsLib.INITIAL_RATE_AT_TARGET), _err(market)), "avgBorrowRate"); assertEq(rateAtTarget, 0, "prevBorrowRate"); } @@ -337,10 +343,12 @@ contract AdaptiveCurveIrmTest is Test { market.totalSupplyAssets = 10 ether; assertGe( - irm.borrowRateView(marketParams, market), uint256(ConstantsLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) + irm.borrowRateView(marketParams, market), + uint256(ConstantsLib.MIN_RATE_AT_TARGET.wDivToZero(ConstantsLib.CURVE_STEEPNESS)) ); assertGe( - irm.borrowRate(marketParams, market), uint256(ConstantsLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS)) + irm.borrowRate(marketParams, market), + uint256(ConstantsLib.MIN_RATE_AT_TARGET.wDivToZero(ConstantsLib.CURVE_STEEPNESS)) ); } @@ -350,32 +358,45 @@ contract AdaptiveCurveIrmTest is Test { market.totalSupplyAssets = 10 ether; assertLe( - irm.borrowRateView(marketParams, market), uint256(ConstantsLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) + irm.borrowRateView(marketParams, market), + uint256(ConstantsLib.MAX_RATE_AT_TARGET.wMulToZero(ConstantsLib.CURVE_STEEPNESS)) ); assertLe( - irm.borrowRate(marketParams, market), uint256(ConstantsLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS)) + irm.borrowRate(marketParams, market), + uint256(ConstantsLib.MAX_RATE_AT_TARGET.wMulToZero(ConstantsLib.CURVE_STEEPNESS)) ); } + function testConstants() public { + assertGe(ConstantsLib.CURVE_STEEPNESS, 1 ether, "curveSteepness too small"); + assertLe(ConstantsLib.CURVE_STEEPNESS, 100 ether, "curveSteepness too big"); + assertGe(ConstantsLib.ADJUSTMENT_SPEED, 0, "adjustmentSpeed too small"); + assertLe(ConstantsLib.ADJUSTMENT_SPEED, int256(1_000 ether) / 365 days, "adjustmentSpeed too big"); + assertGt(ConstantsLib.TARGET_UTILIZATION, 0, "targetUtilization too small"); + assertLt(ConstantsLib.TARGET_UTILIZATION, 1 ether, "targetUtilization too big"); + assertGe(ConstantsLib.INITIAL_RATE_AT_TARGET, ConstantsLib.MIN_RATE_AT_TARGET, "initialRateAtTarget too small"); + assertLe(ConstantsLib.INITIAL_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET, "initialRateAtTarget too large"); + } + /* HELPERS */ function _expectedRateAtTarget(Id id, Market memory market) internal view returns (int256) { - int256 rateAtTarget = int256(irm.rateAtTarget(id)); - int256 speed = ADJUSTMENT_SPEED.wMulDown(_err(market)); + int256 rateAtTarget = irm.rateAtTarget(id); + int256 speed = ConstantsLib.ADJUSTMENT_SPEED.wMulToZero(_err(market)); uint256 elapsed = (rateAtTarget > 0) ? block.timestamp - market.lastUpdate : 0; int256 linearAdaptation = speed * int256(elapsed); int256 adaptationMultiplier = ExpLib.wExp(linearAdaptation); return (rateAtTarget > 0) - ? rateAtTarget.wMulDown(adaptationMultiplier).bound( + ? rateAtTarget.wMulToZero(adaptationMultiplier).bound( ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET ) - : INITIAL_RATE_AT_TARGET; + : ConstantsLib.INITIAL_RATE_AT_TARGET; } function _expectedAvgRate(Id id, Market memory market) internal view returns (uint256) { - int256 rateAtTarget = int256(irm.rateAtTarget(id)); + int256 rateAtTarget = irm.rateAtTarget(id); int256 err = _err(market); - int256 speed = ADJUSTMENT_SPEED.wMulDown(err); + int256 speed = ConstantsLib.ADJUSTMENT_SPEED.wMulToZero(err); uint256 elapsed = (rateAtTarget > 0) ? block.timestamp - market.lastUpdate : 0; int256 linearAdaptation = speed * int256(elapsed); int256 endRateAtTarget = int256(_expectedRateAtTarget(id, market)); @@ -387,7 +408,7 @@ contract AdaptiveCurveIrmTest is Test { } else { // Safe "unchecked" cast to uint256 because linearAdaptation < 0 <=> newBorrowRate <= borrowRateAfterJump. avgBorrowRate = - uint256((int256(newBorrowRate) - int256(_curve(rateAtTarget, err))).wDivDown(linearAdaptation)); + uint256((int256(newBorrowRate) - int256(_curve(rateAtTarget, err))).wDivToZero(linearAdaptation)); } return avgBorrowRate; } @@ -395,9 +416,11 @@ contract AdaptiveCurveIrmTest is Test { function _curve(int256 rateAtTarget, int256 err) internal pure returns (uint256) { // Safe "unchecked" cast because err >= -1 (in WAD). if (err < 0) { - return uint256(((WAD - WAD.wDivDown(CURVE_STEEPNESS)).wMulDown(err) + WAD).wMulDown(rateAtTarget)); + return uint256( + ((WAD - WAD.wDivToZero(ConstantsLib.CURVE_STEEPNESS)).wMulToZero(err) + WAD).wMulToZero(rateAtTarget) + ); } else { - return uint256(((CURVE_STEEPNESS - WAD).wMulDown(err) + WAD).wMulDown(rateAtTarget)); + return uint256(((ConstantsLib.CURVE_STEEPNESS - WAD).wMulToZero(err) + WAD).wMulToZero(rateAtTarget)); } } @@ -406,10 +429,10 @@ contract AdaptiveCurveIrmTest is Test { int256 utilization = int256(market.totalBorrowAssets.wDivDown(market.totalSupplyAssets)); - if (utilization > TARGET_UTILIZATION) { - err = (utilization - TARGET_UTILIZATION).wDivDown(WAD - TARGET_UTILIZATION); + if (utilization > ConstantsLib.TARGET_UTILIZATION) { + err = (utilization - ConstantsLib.TARGET_UTILIZATION).wDivToZero(WAD - ConstantsLib.TARGET_UTILIZATION); } else { - err = (utilization - TARGET_UTILIZATION).wDivDown(TARGET_UTILIZATION); + err = (utilization - ConstantsLib.TARGET_UTILIZATION).wDivToZero(ConstantsLib.TARGET_UTILIZATION); } } } diff --git a/test/forge/ExpLibTest.sol b/test/forge/ExpLibTest.sol index 3fab4ca2..ee61ce06 100644 --- a/test/forge/ExpLibTest.sol +++ b/test/forge/ExpLibTest.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.0; import {MathLib, WAD_INT} from "../../src/libraries/MathLib.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 {ConstantsLib} from "../../src/libraries/adaptive-curve/ConstantsLib.sol"; import {MathLib as MorphoMathLib} from "../../lib/morpho-blue/src/libraries/MathLib.sol"; import "../../lib/forge-std/src/Test.sol"; @@ -62,8 +62,8 @@ 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 testWExpWMulMaxRate() public pure { + ExpLib.wExp(ExpLib.WEXP_UPPER_BOUND).wMulToZero(ConstantsLib.MAX_RATE_AT_TARGET); } function _wExpUnbounded(int256 x) internal pure returns (int256) { diff --git a/test/hardhat/irm/Irm.spec.ts b/test/hardhat/irm/Irm.spec.ts index 3be2495b..b5004a93 100644 --- a/test/hardhat/irm/Irm.spec.ts +++ b/test/hardhat/irm/Irm.spec.ts @@ -50,13 +50,7 @@ describe("irm", () => { const AdaptiveCurveIrmFactory = await hre.ethers.getContractFactory("AdaptiveCurveIrm", admin); - irm = await AdaptiveCurveIrmFactory.deploy( - await admin.getAddress(), - 4000000000000000000n, - 1585489599188n, - 900000000000000000n, - 317097919n, - ); + irm = await AdaptiveCurveIrmFactory.deploy(await admin.getAddress()); const irmAddress = await irm.getAddress();