Skip to content

Commit

Permalink
Merge pull request #66 from morpho-org/fix/bound-constructor
Browse files Browse the repository at this point in the history
fix(constructor): add parameter bounds & extract constants / wExp
  • Loading branch information
MerlinEgalite authored Nov 16, 2023
2 parents 1fc83bc + f158703 commit c2b1732
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 129 deletions.
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;

/// @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 @@ -312,10 +311,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 @@ -339,17 +334,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 @@ -359,9 +362,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

0 comments on commit c2b1732

Please sign in to comment.