Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(constructor): add parameter bounds & extract constants / wExp #66

Merged
merged 11 commits into from
Nov 16, 2023
28 changes: 17 additions & 11 deletions src/SpeedJumpIrm.sol → src/AdaptiveCurveIrm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +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/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";
Expand All @@ -25,25 +27,25 @@ contract AdaptiveCurveIrm is IIrm {
/// @notice Emitted when a borrow rate is updated.
event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 rateAtTarget);

/* CONSTANTS */
/* IMMUTABLES */

/// @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.
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;
Expand Down Expand Up @@ -71,11 +73,13 @@ contract AdaptiveCurveIrm is IIrm {
) {
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 >= MIN_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_SMALL);
require(initialRateAtTarget <= 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;
Expand Down Expand Up @@ -179,6 +183,8 @@ contract AdaptiveCurveIrm is IIrm {
/// 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);
return startRateAtTarget.wMulDown(ExpLib.wExp(linearAdaptation)).bound(
ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET
);
}
}
45 changes: 1 addition & 44 deletions src/libraries/MathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
19 changes: 19 additions & 0 deletions src/libraries/adaptive-curve/ConstantsLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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;

/// @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;
}
49 changes: 49 additions & 0 deletions src/libraries/adaptive-curve/ExpLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
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 = 57716089161558943949701069502944508345128.422502756744429568 ether;
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved

/// @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);
}
}
}
29 changes: 17 additions & 12 deletions test/SpeedJumpIrmTest.sol → test/AdaptiveCurveIrmTest.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../src/SpeedJumpIrm.sol";
import "../src/AdaptiveCurveIrm.sol";

import "../lib/forge-std/src/Test.sol";

contract AdaptiveCurveIrmTest is Test {
using MathLib for int256;
using MathLib for int256;
using MathLib for uint256;
using UtilsLib for int256;
Expand Down Expand Up @@ -216,10 +215,6 @@ contract AdaptiveCurveIrmTest 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());
}

/* HANDLERS */

function handleBorrowRate(uint256 totalSupplyAssets, uint256 totalBorrowAssets, uint256 elapsed) external {
Expand All @@ -243,17 +238,25 @@ contract AdaptiveCurveIrmTest 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(ConstantsLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS))
);
assertGe(
irm.borrowRate(marketParams, market), uint256(ConstantsLib.MIN_RATE_AT_TARGET.wDivDown(CURVE_STEEPNESS))
);
}

function invariantLeMaxRateAtTarget() public {
Market memory market;
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(ConstantsLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS))
);
assertLe(
irm.borrowRate(marketParams, market), uint256(ConstantsLib.MAX_RATE_AT_TARGET.wMulDown(CURVE_STEEPNESS))
);
}

/* HELPERS */
Expand All @@ -263,9 +266,11 @@ contract AdaptiveCurveIrmTest 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(irm.MIN_RATE_AT_TARGET(), irm.MAX_RATE_AT_TARGET())
? rateAtTarget.wMulDown(adaptationMultiplier).bound(
ConstantsLib.MIN_RATE_AT_TARGET, ConstantsLib.MAX_RATE_AT_TARGET
)
: INITIAL_RATE_AT_TARGET;
}

Expand Down
88 changes: 88 additions & 0 deletions test/ExpLibTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
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 {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;

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);
assertEq(_wExpUnbounded(ExpLib.WEXP_UPPER_BOUND), ExpLib.WEXP_UPPER_VALUE);
}

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);
}

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);
}
}
}
Loading