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/AdaptativeCurveIrm.sol
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
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/adaptative-curve/ExpLib.sol";
import {ConstantsLib} from "./libraries/adaptative-curve/ConstantsLib.sol";
import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib.sol";
import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol";
import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol";
Expand All @@ -29,25 +31,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 @@ -75,11 +77,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 @@ -183,6 +187,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
16 changes: 16 additions & 0 deletions src/libraries/adaptative-curve/ConstantsLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

library ConstantsLib {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we need a constantLib for the IRM. Everything in the same file is great

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a file is handy for integrators (ourselves) and having it in an dedicated file avoids having to compile the IRM in case the constants are used by integrators (including ourselves)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and if we do follow this suggestion then there should be only 2 constants left in this lib and those would almost never be useful to integrators (who cares about the max curve steepness, you only really care about the curve steepness of the IRM you are interacting with)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same arguments could be used for Blue but still we extracted constants to a file because it's shown to be handy for our test suite alone ; having constants defined in IRM (such as the min/max rates) require to deploy the contract to test things using these bounds... For example what we do in ExpLib but thanks to the constants library we don't need to deploy an IRM to test the upper value of wExp

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not saying that we should never extract constants. My point is that min/max rates should really be immutables, and I think that the other constants would almost never be useful

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me the benefit of having constants defined in a dedicated library and being able to access it outside of the IRM outweighs the loss of conciseness (now the IRM depends on 1 more file)

Whether min/max rates should be immutable is another topic that I'm open to discuss in a dedicated issue

Copy link
Contributor

@QGarchery QGarchery Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opened #96

I think that those topics are not so independent, but I'm fine with the current code in any case

/// @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;
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

/// @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/adaptative-curve/ExpLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: UNLICENSED
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this value was changed because the previous value didn't correspond to the bound from the unbounded wExp

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except that the code is correct (just checked)


/// @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/AdaptativeCurveIrmTest.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/AdaptativeCurveIrm.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: UNLICENSED
pragma solidity ^0.8.0;

import {MathLib, WAD_INT} from "../src/libraries/MathLib.sol";
import {ConstantsLib} from "../src/libraries/adaptative-curve/ConstantsLib.sol";
import {ExpLib} from "../src/libraries/adaptative-curve/ExpLib.sol";
import {wadExp} from "../lib/solmate/src/utils/SignedWadMath.sol";
import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol";

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

contract ExpLibTest is Test {
using MathLib for int256;
using MorphoMathLib for uint256;

/// @dev ln(1e-9) truncated at 2 decimal places.
int256 internal constant LN_GWEI_INT = -20.72 ether;

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