diff --git a/foundry.toml b/foundry.toml index 0147431f..8318e07d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,9 @@ libs = ["lib"] [profile.default.fuzz] runs = 4096 +[profile.default.invariant] +fail_on_revert = true + [profile.default.fmt] wrap_comments = true @@ -20,5 +23,13 @@ script = "/dev/null" [profile.test] via-ir = false +[profile.test.fuzz] +runs = 16384 + +[profile.test.invariant] +runs = 32 +depth = 1024 +fail_on_revert = true + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/morpho-blue b/lib/morpho-blue index f2ed54b7..f463e40f 160000 --- a/lib/morpho-blue +++ b/lib/morpho-blue @@ -1 +1 @@ -Subproject commit f2ed54b74b8226bc34dcc1558271b89836bc4e55 +Subproject commit f463e40f776acd0f26d0d380b51cfd02949c8c23 diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 56c1ddae..3ea03046 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -3,28 +3,19 @@ pragma solidity 0.8.19; import {IIrm} from "../lib/morpho-blue/src/interfaces/IIrm.sol"; -import {MathLib} from "./libraries/MathLib.sol"; import {UtilsLib} from "./libraries/UtilsLib.sol"; import {ErrorsLib} from "./libraries/ErrorsLib.sol"; +import {MathLib, WAD_INT as WAD} from "./libraries/MathLib.sol"; import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib.sol"; import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; -import {WAD, MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; +import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; -struct MarketIrm { - // Previous final borrow rate. Scaled by WAD. - uint128 prevBorrowRate; - // Previous error. Scaled by WAD. - int128 prevErr; -} - -/// @title SpeedJumpIrm +/// @title AdaptiveCurveIrm /// @author Morpho Labs /// @custom:contact security@morpho.org -/// @notice Interest rate model. -contract SpeedJumpIrm is IIrm { +contract AdaptiveCurveIrm is IIrm { using MathLib for int256; - using MathLib for uint256; - using UtilsLib for uint256; + using UtilsLib for int256; using MorphoMathLib for uint128; using MorphoMathLib for uint256; using MarketParamsLib for MarketParams; @@ -32,65 +23,73 @@ contract SpeedJumpIrm is IIrm { /* EVENTS */ /// @notice Emitted when a borrow rate is updated. - event BorrowRateUpdate(Id indexed id, int128 err, uint128 newBorrowRate, uint256 avgBorrowRate); + event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 rateAtTarget); /* CONSTANTS */ - /// @notice Maximum rate per second (scaled by WAD) (1B% APR). - uint256 public constant MAX_RATE = uint256(1e7 ether) / 365 days; - /// @notice Mininimum rate per second (scaled by WAD) (0.1% APR). - uint256 public constant MIN_RATE = uint256(0.001 ether) / 365 days; + /// @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 Ln of the jump factor (scaled by WAD). - uint256 public immutable LN_JUMP_FACTOR; - /// @notice Speed factor (scaled by WAD). - /// @dev The speed is per second, so the rate moves at a speed of SPEED_FACTOR * err each second (while being - /// continuously compounded). A typical value for the SPEED_FACTOR would be 10 ethers / 365 days. - uint256 public immutable SPEED_FACTOR; + /// @notice Curve steepness (scaled by WAD). + /// @dev Verified to be greater than 1 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. + int256 public immutable ADJUSTMENT_SPEED; /// @notice Target utilization (scaled by WAD). - uint256 public immutable TARGET_UTILIZATION; - /// @notice Initial rate (scaled by WAD). - uint128 public immutable INITIAL_RATE; + /// @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 IRM storage for each market. - mapping(Id => MarketIrm) public marketIrm; + /// @notice Rate at target utilization. + /// @dev Tells the height of the curve. + mapping(Id => int256) public rateAtTarget; /* CONSTRUCTOR */ /// @notice Constructor. /// @param morpho The address of Morpho. - /// @param lnJumpFactor The log of the jump factor (scaled by WAD). - /// @param speedFactor The speed factor (scaled by WAD). - /// @param targetUtilization The target utilization (scaled by WAD). Should be strictly between 0 and 1. - /// @param initialRate The initial rate (scaled by WAD). + /// @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, - uint256 lnJumpFactor, - uint256 speedFactor, - uint256 targetUtilization, - uint128 initialRate + int256 curveSteepness, + int256 adjustmentSpeed, + int256 targetUtilization, + int256 initialRateAtTarget ) { - require(lnJumpFactor <= uint256(type(int256).max), ErrorsLib.INPUT_TOO_LARGE); - require(speedFactor <= uint256(type(int256).max), ErrorsLib.INPUT_TOO_LARGE); + require(morpho != address(0), ErrorsLib.ZERO_ADDRESS); + require(curveSteepness >= WAD, ErrorsLib.INPUT_TOO_SMALL); + require(adjustmentSpeed >= 0, ErrorsLib.INPUT_TOO_SMALL); 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); MORPHO = morpho; - LN_JUMP_FACTOR = lnJumpFactor; - SPEED_FACTOR = speedFactor; + CURVE_STEEPNESS = curveSteepness; + ADJUSTMENT_SPEED = adjustmentSpeed; TARGET_UTILIZATION = targetUtilization; - INITIAL_RATE = initialRate; + INITIAL_RATE_AT_TARGET = initialRateAtTarget; } /* BORROW RATES */ /// @inheritdoc IIrm function borrowRateView(MarketParams memory marketParams, Market memory market) external view returns (uint256) { - (,, uint256 avgBorrowRate) = _borrowRate(marketParams.id(), market); - return avgBorrowRate; + (uint256 avgRate,) = _borrowRate(marketParams.id(), market); + return avgRate; } /// @inheritdoc IIrm @@ -99,57 +98,87 @@ contract SpeedJumpIrm is IIrm { Id id = marketParams.id(); - (int128 err, uint128 newBorrowRate, uint256 avgBorrowRate) = _borrowRate(id, market); + (uint256 avgRate, int256 endRateAtTarget) = _borrowRate(id, market); + + rateAtTarget[id] = endRateAtTarget; - marketIrm[id].prevErr = err; - marketIrm[id].prevBorrowRate = newBorrowRate; + // Safe "unchecked" cast because endRateAtTarget >= 0. + emit BorrowRateUpdate(id, avgRate, uint256(endRateAtTarget)); - emit BorrowRateUpdate(id, err, newBorrowRate, avgBorrowRate); + return avgRate; + } + + /// @dev Returns avgRate and endRateAtTarget. + /// @dev Assumes that the inputs `marketParams` and `id` match. + function _borrowRate(Id id, Market memory market) private view returns (uint256, int256) { + // Safe "unchecked" cast because the utilization is smaller than 1 (scaled by WAD). + 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 startRateAtTarget = rateAtTarget[id]; + + int256 avgRateAtTarget; + int256 endRateAtTarget; + + if (startRateAtTarget == 0) { + // First interaction. + avgRateAtTarget = INITIAL_RATE_AT_TARGET; + endRateAtTarget = 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); + // 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); + int256 linearAdaptation = speed * elapsed; + + if (linearAdaptation == 0) { + // If linearAdaptation == 0, avgRateAtTarget = endRateAtTarget = startRateAtTarget; + avgRateAtTarget = startRateAtTarget; + endRateAtTarget = startRateAtTarget; + } else { + // Formula of the average rate that should be returned to Morpho Blue: + // avg = 1/T * ∫_0^T curve(startRateAtTarget*exp(speed*x), err) dx + // The integral is approximated with the trapezoidal rule: + // avg ~= 1/T * Σ_i=1^N [curve(f((i-1) * T/N), err) + curve(f(i * T/N), err)] / 2 * T/N + // Where f(x) = startRateAtTarget*exp(speed*x) + // avg ~= Σ_i=1^N [curve(f((i-1) * T/N), err) + curve(f(i * T/N), err)] / (2 * N) + // As curve is linear in its first argument: + // avg ~= curve([Σ_i=1^N [f((i-1) * T/N) + f(i * T/N)] / (2 * N), err) + // avg ~= curve([(f(0) + f(T))/2 + Σ_i=1^(N-1) f(i * T/N)] / N, err) + // avg ~= curve([(startRateAtTarget + endRateAtTarget)/2 + Σ_i=1^(N-1) f(i * T/N)] / N, err) + // With N = 2: + // avg ~= curve([(startRateAtTarget + endRateAtTarget)/2 + startRateAtTarget*exp(speed*T/2)] / 2, err) + // avg ~= curve([startRateAtTarget + endRateAtTarget + 2*startRateAtTarget*exp(speed*T/2)] / 4, err) + endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); + int256 midRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation / 2); + avgRateAtTarget = (startRateAtTarget + endRateAtTarget + 2 * midRateAtTarget) / 4; + } + } + + // Safe "unchecked" cast because avgRateAtTarget >= 0. + return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); + } - return avgBorrowRate; + /// @dev Returns the rate for a given `_rateAtTarget` and an `err`. + /// 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) { + // Non negative because 1 - 1/C >= 0, C - 1 >= 0. + int256 coeff = err < 0 ? WAD - WAD.wDivDown(CURVE_STEEPNESS) : CURVE_STEEPNESS - WAD; + // Non negative if _rateAtTarget >= 0 because if err < 0, coeff <= 1. + return (coeff.wMulDown(err) + WAD).wMulDown(int256(_rateAtTarget)); } - /// @dev Returns err, newBorrowRate and avgBorrowRate. - function _borrowRate(Id id, Market memory market) private view returns (int128, uint128, uint128) { - uint256 utilization = - market.totalSupplyAssets > 0 ? market.totalBorrowAssets.wDivDown(market.totalSupplyAssets) : 0; - - uint256 errNormFactor = utilization > TARGET_UTILIZATION ? WAD - TARGET_UTILIZATION : TARGET_UTILIZATION; - // Safe "unchecked" int128 cast because |err| <= WAD. - // Safe "unchecked" int256 casts because utilization <= WAD, TARGET_UTILIZATION < WAD and errNormFactor <= WAD. - int128 err = int128((int256(utilization) - int256(TARGET_UTILIZATION)).wDivDown(int256(errNormFactor))); - - MarketIrm storage irm = marketIrm[id]; - if (irm.prevBorrowRate == 0) return (err, INITIAL_RATE, INITIAL_RATE); - - // errDelta = err - prevErr. - // errDelta is between -1 and 1, scaled by WAD. - int256 errDelta = err - irm.prevErr; - - // Safe "unchecked" cast because LN_JUMP_FACTOR <= type(int256).max. - uint256 jumpMultiplier = MathLib.wExp(errDelta.wMulDown(int256(LN_JUMP_FACTOR))); - // Safe "unchecked" cast because SPEED_FACTOR <= type(int256).max. - int256 speed = int256(SPEED_FACTOR).wMulDown(err); - uint256 elapsed = block.timestamp - market.lastUpdate; - // Safe "unchecked" cast because elapsed <= block.timestamp. - int256 linearVariation = speed * int256(elapsed); - uint256 variationMultiplier = MathLib.wExp(linearVariation); - - // newBorrowRate = prevBorrowRate * jumpMultiplier * variationMultiplier. - uint256 borrowRateAfterJump = irm.prevBorrowRate.wMulDown(jumpMultiplier); - uint256 newBorrowRate = borrowRateAfterJump.wMulDown(variationMultiplier); - - // Then we compute the average rate over the period (this is what Morpho needs to accrue the interest). - // avgBorrowRate = 1 / elapsed * ∫ borrowRateAfterJump * exp(speed * t) dt between 0 and elapsed - // = borrowRateAfterJump * (exp(linearVariation) - 1) / linearVariation - // = (newBorrowRate - borrowRateAfterJump) / linearVariation - // And avgBorrowRate ~ borrowRateAfterJump for linearVariation around zero. - uint256 avgBorrowRate; - if (linearVariation == 0) avgBorrowRate = borrowRateAfterJump; - // Safe "unchecked" cast to uint256 because linearVariation < 0 <=> newBorrowRate <= borrowRateAfterJump. - else avgBorrowRate = uint256((int256(newBorrowRate) - int256(borrowRateAfterJump)).wDivDown(linearVariation)); - - // We bound both newBorrowRate and avgBorrowRate between MIN_RATE and MAX_RATE. - return (err, uint128(newBorrowRate.bound(MIN_RATE, MAX_RATE)), uint128(avgBorrowRate.bound(MIN_RATE, MAX_RATE))); + /// @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(MathLib.wExp(linearAdaptation)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); } } diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 612d156c..8a88f349 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -9,14 +9,14 @@ 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 passing the zero input. - string internal constant ZERO_INPUT = "zero input"; + /// @dev Thrown when the input is too small. + string internal constant INPUT_TOO_SMALL = "input too small"; - /// @dev Thrown when wExp underflows. - string internal constant WEXP_UNDERFLOW = "wExp underflow"; + /// @dev Thrown when passing the zero address. + string internal constant ZERO_ADDRESS = "zero address"; - /// @dev Thrown when wExp overflows. - string internal constant WEXP_OVERFLOW = "wExp overflow"; + /// @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 3bd37551..120c7756 100644 --- a/src/libraries/MathLib.sol +++ b/src/libraries/MathLib.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {ErrorsLib} from "./ErrorsLib.sol"; import {WAD} from "../../lib/morpho-blue/src/libraries/MathLib.sol"; int256 constant WAD_INT = int256(WAD); @@ -14,30 +13,39 @@ library MathLib { using MathLib for uint128; using MathLib for uint256; using {wDivDown} for int256; - using {wMulDown} for int256; /// @dev ln(2). - int256 private constant LN2_INT = 0.693147180559945309 ether; + 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 (uint256) { + function wExp(int256 x) internal pure returns (int256) { unchecked { - // Revert if x > ln(2^256-1) ~ 177. - require(x <= 177.44567822334599921 ether, ErrorsLib.WEXP_OVERFLOW); - // Revert if x < -(2**255-1) + (ln(2)/2). - require(x >= type(int256).min + LN2_INT / 2, ErrorsLib.WEXP_UNDERFLOW); + // 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) ? -(LN2_INT / 2) : (LN2_INT / 2); + int256 roundingAdjustment = (x < 0) ? -(LN_2_INT / 2) : (LN_2_INT / 2); // Safe unchecked because x is bounded. - int256 q = (x + roundingAdjustment) / LN2_INT; - // Safe unchecked because |q * LN2_INT| <= |x|. - int256 r = x - q * LN2_INT; + 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| < 1, expR < 2 and the sum is positive. - uint256 expR = uint256(WAD_INT + r + r.wMulDown(r) / 2); + // 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); diff --git a/src/libraries/UtilsLib.sol b/src/libraries/UtilsLib.sol index f6482664..1eae55dd 100644 --- a/src/libraries/UtilsLib.sol +++ b/src/libraries/UtilsLib.sol @@ -8,12 +8,12 @@ pragma solidity ^0.8.0; library UtilsLib { /// @dev Bounds `x` between `low` and `high`. /// @dev Assumes that `low` <= `high`. If it is not the case it returns `low`. - function bound(uint256 x, uint256 low, uint256 high) internal pure returns (uint256 z) { + function bound(int256 x, int256 low, int256 high) internal pure returns (int256 z) { assembly { // z = min(x, high). - z := xor(x, mul(xor(x, high), lt(high, x))) + z := xor(x, mul(xor(x, high), slt(high, x))) // z = max(z, low). - z := xor(z, mul(xor(z, low), gt(low, z))) + z := xor(z, mul(xor(z, low), sgt(low, z))) } } } diff --git a/test/MathLibTest.sol b/test/MathLibTest.sol index 6d327ea9..4f11dc2b 100644 --- a/test/MathLibTest.sol +++ b/test/MathLibTest.sol @@ -5,37 +5,58 @@ import {MathLib} from "../src/libraries/MathLib.sol"; import {ErrorsLib} from "../src/libraries/ErrorsLib.sol"; import {wadExp} from "../lib/solmate/src/utils/SignedWadMath.sol"; +import {AdaptiveCurveIrm} from "../src/SpeedJumpIrm.sol"; import "../lib/forge-std/src/Test.sol"; contract MathLibTest is Test { using MathLib for uint128; using MathLib for uint256; - int256 private constant LN2_INT = 0.693147180559945309 ether; + /// @dev ln(1e-9) truncated at 2 decimal places. + int256 internal constant LN_GWEI_INT = -20.72 ether; function testWExp(int256 x) public { - // Bound between ln(1e-9) ~ -27 and ln(max / 1e18 / 1e18) ~ 94, to be able to use `assertApproxEqRel`. - x = bound(x, -27 ether, 94 ether); - assertApproxEqRel(MathLib.wExp(x), uint256(wadExp(x)), 0.01 ether); + // 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 { - // Bound between -(2**255-1) + ln(2)/2 and ln(1e-18). - x = bound(x, type(int256).min + LN2_INT / 2, -178 ether); - assertEq(MathLib.wExp(x), 0); + x = bound(x, MathLib.LN_WEI_INT, LN_GWEI_INT); + + assertApproxEqAbs(MathLib.wExp(x), 0, 1e10); } function testWExpTooSmall(int256 x) public { - // Bound between -(2**255-1) and -(2**255-1) + ln(2)/2 - 1. - x = bound(x, type(int256).min, type(int256).min + LN2_INT / 2 - 1); - vm.expectRevert(bytes(ErrorsLib.WEXP_UNDERFLOW)); + x = bound(x, type(int256).min, MathLib.LN_WEI_INT); + assertEq(MathLib.wExp(x), 0); } function testWExpTooLarge(int256 x) public { - // Bound between ln(2**256-1) ~ 177 and 2**255-1. - x = bound(x, 178 ether, type(int256).max); - vm.expectRevert(bytes(ErrorsLib.WEXP_OVERFLOW)); - MathLib.wExp(x); + 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); } } diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index 4810ccf3..fdf3b864 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -5,28 +5,120 @@ import "../src/SpeedJumpIrm.sol"; import "../lib/forge-std/src/Test.sol"; -contract SpeedJumpIrmTest is Test { +contract AdaptiveCurveIrmTest is Test { using MathLib for int256; using MathLib for int256; using MathLib for uint256; - using UtilsLib for uint256; + using UtilsLib for int256; using MorphoMathLib for uint128; using MorphoMathLib for uint256; using MarketParamsLib for MarketParams; - event BorrowRateUpdate(Id indexed id, int128 err, uint128 newBorrowRate, uint256 avgBorrowRate); + event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 rateAtTarget); - uint256 internal constant LN2 = 0.69314718056 ether; - uint256 internal constant TARGET_UTILIZATION = 0.8 ether; - uint256 internal constant SPEED_FACTOR = uint256(0.01 ether) / uint256(10 hours); - uint128 internal constant INITIAL_RATE = uint128(0.01 ether) / uint128(365 days); + 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; - SpeedJumpIrm internal irm; + AdaptiveCurveIrm internal irm; MarketParams internal marketParams = MarketParams(address(0), address(0), address(0), address(0), 0); function setUp() public { - irm = new SpeedJumpIrm(address(this), LN2, SPEED_FACTOR, TARGET_UTILIZATION, INITIAL_RATE); + irm = + new AdaptiveCurveIrm(address(this), CURVE_STEEPNESS, ADJUSTMENT_SPEED, TARGET_UTILIZATION, INITIAL_RATE_AT_TARGET); vm.warp(90 days); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = AdaptiveCurveIrmTest.handleBorrowRate.selector; + targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); + targetContract(address(this)); + } + + /* TESTS */ + + function testDeployment() public { + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + new AdaptiveCurveIrm(address(0), 0, 0, 0, 0); + } + + function testFirstBorrowRateUtilizationZero() public { + Market memory market; + + assertApproxEqRel( + irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET / 4), 0.0001 ether, "avgBorrowRate" + ); + assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET, "rateAtTarget"); + } + + function testFirstBorrowRateUtilizationOne() public { + Market memory market; + 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"); + } + + function testFirstBorrowRateUtilizationTarget() public { + Market memory market; + market.totalBorrowAssets = 0.9 ether; + market.totalSupplyAssets = 1 ether; + + assertEq(irm.borrowRate(marketParams, market), uint256(INITIAL_RATE_AT_TARGET), "avgBorrowRate"); + assertEq(irm.rateAtTarget(marketParams.id()), 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); + + market.totalBorrowAssets = 1 ether; + market.totalSupplyAssets = 1 ether; + market.lastUpdate = uint128(block.timestamp - 5 days); + + // (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))), + 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)), + 0.1 ether + ); + // Expected rate: 5.744%. + assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.05744 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); + + market.totalBorrowAssets = 0 ether; + market.totalSupplyAssets = 1 ether; + market.lastUpdate = uint128(block.timestamp - 5 days); + + // (exp((-50/365)*5) ~= 0.5041. + assertApproxEqRel( + irm.borrowRateView(marketParams, market), + uint256( + (INITIAL_RATE_AT_TARGET / 4).wMulDown((0.5041 ether - 1 ether) * WAD / (-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)), + 0.1 ether + ); + // Expected rate: 0.181%. + assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.00181 ether) / 365 days, 0.1 ether); } function testFirstBorrowRate(Market memory market) public { @@ -34,19 +126,22 @@ contract SpeedJumpIrmTest is Test { vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); uint256 avgBorrowRate = irm.borrowRate(marketParams, market); - (uint256 prevBorrowRate, int256 prevErr) = irm.marketIrm(marketParams.id()); + int256 rateAtTarget = irm.rateAtTarget(marketParams.id()); - assertEq(avgBorrowRate, INITIAL_RATE, "avgBorrowRate"); - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(prevErr, _err(market), "prevErr"); + assertEq(avgBorrowRate, _curve(int256(INITIAL_RATE_AT_TARGET), _err(market)), "avgBorrowRate"); + assertEq(rateAtTarget, INITIAL_RATE_AT_TARGET, "rateAtTarget"); } function testBorrowRateEventEmission(Market memory market) public { vm.assume(market.totalBorrowAssets > 0); vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); - vm.expectEmit(address(irm)); - emit BorrowRateUpdate(marketParams.id(), int128(_err(market)), INITIAL_RATE, INITIAL_RATE); + vm.expectEmit(true, true, true, true, address(irm)); + emit BorrowRateUpdate( + marketParams.id(), + _curve(int256(INITIAL_RATE_AT_TARGET), _err(market)), + uint256(_expectedRateAtTarget(marketParams.id(), market)) + ); irm.borrowRate(marketParams, market); } @@ -55,11 +150,10 @@ contract SpeedJumpIrmTest is Test { vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); uint256 avgBorrowRate = irm.borrowRateView(marketParams, market); - (uint256 prevBorrowRate, int256 prevErr) = irm.marketIrm(marketParams.id()); + int256 rateAtTarget = irm.rateAtTarget(marketParams.id()); - assertEq(avgBorrowRate, INITIAL_RATE, "avgBorrowRate"); - assertEq(prevBorrowRate, 0, "prevBorrowRate"); - assertEq(prevErr, 0, "prevErr"); + assertEq(avgBorrowRate, _curve(int256(INITIAL_RATE_AT_TARGET), _err(market)), "avgBorrowRate"); + assertEq(rateAtTarget, 0, "prevBorrowRate"); } function testBorrowRate(Market memory market0, Market memory market1) public { @@ -69,36 +163,20 @@ contract SpeedJumpIrmTest is Test { vm.assume(market1.totalBorrowAssets > 0); vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); - market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); + market1.lastUpdate = uint128(bound(market1.lastUpdate, block.timestamp - 5 days, block.timestamp - 1)); - uint256 avgBorrowRate = irm.borrowRate(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); + int256 expectedRateAtTarget = _expectedRateAtTarget(marketParams.id(), market1); + uint256 expectedAvgRate = _expectedAvgRate(marketParams.id(), market1); - (uint256 expectedAvgBorrowRate, uint256 expectedPrevBorrowRate) = _expectedBorrowRates(market0, market1); + uint256 borrowRateView = irm.borrowRateView(marketParams, market1); + uint256 borrowRate = irm.borrowRate(marketParams, market1); - assertEq(prevBorrowRate, expectedPrevBorrowRate, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + assertEq(borrowRateView, borrowRate, "borrowRateView"); + assertApproxEqRel(borrowRate, expectedAvgRate, 0.11 ether, "avgBorrowRate"); + assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); } - function testBorrowRateView(Market memory market0, Market memory market1) public { - vm.assume(market0.totalBorrowAssets > 0); - vm.assume(market0.totalSupplyAssets >= market0.totalBorrowAssets); - irm.borrowRate(marketParams, market0); - - vm.assume(market1.totalBorrowAssets > 0); - vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); - market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); - - uint256 avgBorrowRate = irm.borrowRateView(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); - - (uint256 expectedAvgBorrowRate,) = _expectedBorrowRates(market0, market1); - - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); - } - - function testBorrowRateJumpOnly(Market memory market0, Market memory market1) public { + function testBorrowRateNoTimeElapsed(Market memory market0, Market memory market1) public { vm.assume(market0.totalBorrowAssets > 0); vm.assume(market0.totalSupplyAssets >= market0.totalBorrowAssets); irm.borrowRate(marketParams, market0); @@ -107,113 +185,128 @@ contract SpeedJumpIrmTest is Test { vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); market1.lastUpdate = uint128(block.timestamp); - uint256 avgBorrowRate = irm.borrowRate(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); + int256 expectedRateAtTarget = _expectedRateAtTarget(marketParams.id(), market1); + uint256 expectedAvgRate = _expectedAvgRate(marketParams.id(), market1); - (uint256 expectedAvgBorrowRate, uint256 expectedPrevBorrowRate) = _expectedBorrowRates(market0, market1); + uint256 borrowRateView = irm.borrowRateView(marketParams, market1); + uint256 borrowRate = irm.borrowRate(marketParams, market1); - assertEq(expectedAvgBorrowRate, expectedPrevBorrowRate, "expectedAvgBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); - assertEq(prevBorrowRate, expectedPrevBorrowRate, "prevBorrowRate"); + assertEq(borrowRateView, borrowRate, "borrowRateView"); + assertApproxEqRel(borrowRate, expectedAvgRate, 0.01 ether, "avgBorrowRate"); + assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); } - function testBorrowRateViewJumpOnly(Market memory market0, Market memory market1) public { + function testBorrowRateNoUtilizationChange(Market memory market0, Market memory market1) public { vm.assume(market0.totalBorrowAssets > 0); vm.assume(market0.totalSupplyAssets >= market0.totalBorrowAssets); irm.borrowRate(marketParams, market0); - vm.assume(market1.totalBorrowAssets > 0); - vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); - market1.lastUpdate = uint128(block.timestamp); + market1.totalBorrowAssets = market0.totalBorrowAssets; + market1.totalSupplyAssets = market0.totalSupplyAssets; + market1.lastUpdate = uint128(bound(market1.lastUpdate, block.timestamp - 5 days, block.timestamp - 1)); - uint256 avgBorrowRate = irm.borrowRateView(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); + int256 expectedRateAtTarget = _expectedRateAtTarget(marketParams.id(), market1); + uint256 expectedAvgRate = _expectedAvgRate(marketParams.id(), market1); - (uint256 expectedAvgBorrowRate,) = _expectedBorrowRates(market0, market1); + uint256 borrowRateView = irm.borrowRateView(marketParams, market1); + uint256 borrowRate = irm.borrowRate(marketParams, market1); - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + assertEq(borrowRateView, borrowRate, "borrowRateView"); + assertApproxEqRel(borrowRate, expectedAvgRate, 0.1 ether, "avgBorrowRate"); + assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); } - function testBorrowRateSpeedOnly(Market memory market0, Market memory market1) public { - vm.assume(market0.totalBorrowAssets > 0); - vm.assume(market0.totalSupplyAssets >= market0.totalBorrowAssets); - irm.borrowRate(marketParams, market0); + function testWExpWMulDownMaxRate() public view { + MathLib.wExp(MathLib.WEXP_UPPER_BOUND).wMulDown(irm.MAX_RATE_AT_TARGET()); + } - market1.totalBorrowAssets = market0.totalBorrowAssets; - market1.totalSupplyAssets = market0.totalSupplyAssets; - market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); + /* HANDLERS */ - uint256 avgBorrowRate = irm.borrowRate(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); + function handleBorrowRate(uint256 totalSupplyAssets, uint256 totalBorrowAssets, uint256 elapsed) external { + elapsed = bound(elapsed, 0, type(uint48).max); + totalSupplyAssets = bound(totalSupplyAssets, 0, type(uint128).max); + totalBorrowAssets = bound(totalBorrowAssets, 0, totalSupplyAssets); - (uint256 expectedAvgBorrowRate, uint256 expectedPrevBorrowRate) = _expectedBorrowRates(market0, market1); + Market memory market; + market.lastUpdate = uint128(block.timestamp); + market.totalBorrowAssets = uint128(totalSupplyAssets); + market.totalSupplyAssets = uint128(totalBorrowAssets); - assertEq(prevBorrowRate, expectedPrevBorrowRate, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + vm.warp(block.timestamp + elapsed); + irm.borrowRate(marketParams, market); } - function testBorrowRateViewSpeedOnly(Market memory market0, Market memory market1) public { - vm.assume(market0.totalBorrowAssets > 0); - vm.assume(market0.totalSupplyAssets >= market0.totalBorrowAssets); - irm.borrowRate(marketParams, market0); + /* INVARIANTS */ - market1.totalBorrowAssets = market0.totalBorrowAssets; - market1.totalSupplyAssets = market0.totalSupplyAssets; - market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); + function invariantGeMinRateAtTarget() public { + Market memory market; + market.totalBorrowAssets = 9 ether; + market.totalSupplyAssets = 10 ether; - uint256 avgBorrowRate = irm.borrowRateView(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); + 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))); + } - (uint256 expectedAvgBorrowRate,) = _expectedBorrowRates(market0, market1); + function invariantLeMaxRateAtTarget() public { + Market memory market; + market.totalBorrowAssets = 9 ether; + market.totalSupplyAssets = 10 ether; - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + 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))); } - /// @dev Returns the expected `avgBorrowRate` and `prevBorrowRate`. - function _expectedBorrowRates(Market memory market0, Market memory market1) - internal - view - returns (uint256, uint256) - { - int256 err = _err(market1); - int256 prevErr = _err(market0); - int256 errDelta = err - prevErr; - uint256 elapsed = block.timestamp - market1.lastUpdate; - - uint256 jumpMultiplier = MathLib.wExp(errDelta.wMulDown(int256(LN2))); - int256 speed = int256(SPEED_FACTOR).wMulDown(err); - uint256 variationMultiplier = MathLib.wExp(speed * int256(elapsed)); - uint256 expectedBorrowRateAfterJump = INITIAL_RATE.wMulDown(jumpMultiplier); - uint256 expectedNewBorrowRate = INITIAL_RATE.wMulDown(jumpMultiplier).wMulDown(variationMultiplier); + /* 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)); + uint256 elapsed = (rateAtTarget > 0) ? block.timestamp - market.lastUpdate : 0; + 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()) + : INITIAL_RATE_AT_TARGET; + } - uint256 expectedAvgBorrowRate; - if (speed * int256(elapsed) == 0) { - expectedAvgBorrowRate = INITIAL_RATE.wMulDown(jumpMultiplier); + function _expectedAvgRate(Id id, Market memory market) internal view returns (uint256) { + int256 rateAtTarget = int256(irm.rateAtTarget(id)); + int256 err = _err(market); + int256 speed = ADJUSTMENT_SPEED.wMulDown(err); + uint256 elapsed = (rateAtTarget > 0) ? block.timestamp - market.lastUpdate : 0; + int256 linearAdaptation = speed * int256(elapsed); + int256 endRateAtTarget = int256(_expectedRateAtTarget(id, market)); + uint256 newBorrowRate = _curve(endRateAtTarget, err); + + uint256 avgBorrowRate; + if (linearAdaptation == 0 || rateAtTarget == 0) { + avgBorrowRate = newBorrowRate; } else { - expectedAvgBorrowRate = uint256( - (int256(expectedNewBorrowRate) - int256(expectedBorrowRateAfterJump)).wDivDown(speed * int256(elapsed)) - ); + // Safe "unchecked" cast to uint256 because linearAdaptation < 0 <=> newBorrowRate <= borrowRateAfterJump. + avgBorrowRate = + uint256((int256(newBorrowRate) - int256(_curve(rateAtTarget, err))).wDivDown(linearAdaptation)); } + return avgBorrowRate; + } - return ( - expectedAvgBorrowRate.bound(irm.MIN_RATE(), irm.MAX_RATE()), - expectedNewBorrowRate.bound(irm.MIN_RATE(), irm.MAX_RATE()) - ); + 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)); + } else { + return uint256(((CURVE_STEEPNESS - WAD).wMulDown(err) + WAD).wMulDown(rateAtTarget)); + } } - function _err(Market memory market) internal pure returns (int256) { - uint256 utilization = market.totalBorrowAssets.wDivDown(market.totalSupplyAssets); + function _err(Market memory market) internal pure returns (int256 err) { + if (market.totalSupplyAssets == 0) return -1 ether; + + int256 utilization = int256(market.totalBorrowAssets.wDivDown(market.totalSupplyAssets)); - int256 err; if (utilization > TARGET_UTILIZATION) { - // Safe "unchecked" cast because |err| <= WAD. - err = int256((utilization - TARGET_UTILIZATION).wDivDown(WAD - TARGET_UTILIZATION)); + err = (utilization - TARGET_UTILIZATION).wDivDown(WAD - TARGET_UTILIZATION); } else { - // Safe "unchecked" casts because utilization <= WAD and TARGET_UTILIZATION <= WAD. - err = (int256(utilization) - int256(TARGET_UTILIZATION)).wDivDown(int256(TARGET_UTILIZATION)); + err = (utilization - TARGET_UTILIZATION).wDivDown(TARGET_UTILIZATION); } - return err; } } diff --git a/test/UtilsLibTest.sol b/test/UtilsLibTest.sol index a002b0e8..41e9c8ab 100644 --- a/test/UtilsLibTest.sol +++ b/test/UtilsLibTest.sol @@ -6,9 +6,9 @@ import "../src/libraries/UtilsLib.sol"; import "../lib/forge-std/src/Test.sol"; contract UtilsTest is Test { - using UtilsLib for uint256; + using UtilsLib for int256; - function testBound(uint256 x, uint256 low, uint256 high) public { + function testBound(int256 x, int256 low, int256 high) public { if (x <= high) { if (x >= low) assertEq(x.bound(low, high), x); else assertEq(x.bound(low, high), low);