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

perf wExp #12

Merged
merged 16 commits into from
Sep 4, 2023
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "lib/morpho-blue"]
path = lib/morpho-blue
url = https://github.com/morpho-labs/morpho-blue
[submodule "lib/solmate"]
path = lib/solmate
url = https://github.com/transmissions11/solmate
1 change: 1 addition & 0 deletions lib/solmate
Submodule solmate added at fadb2e
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ forge-std/=lib/forge-std/src/
morpho-blue/=lib/morpho-blue/src/
openzeppelin-contracts/=lib/morpho-blue/lib/openzeppelin-contracts/
openzeppelin/=lib/morpho-blue/lib/openzeppelin-contracts/contracts/
solmate/=lib/solmate/src/
10 changes: 4 additions & 6 deletions src/irm/Irm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,8 @@ contract Irm is IIrm {

/// @notice Constructor.
/// @param morpho The address of Morpho.
/// @param lnJumpFactor The log of the jump factor (scaled by WAD). Warning: lnJumpFactor <= 3 must hold. Above
/// that, the approximations in wExp are considered too large.
/// @param speedFactor The speed factor (scaled by WAD). Warning: |speedFactor * error * elapsed| <= 3 must hold.
/// Above that, the approximations in wExp are considered too large.
/// @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 between 0 and 1.
/// @param initialRate The initial rate (scaled by WAD).
constructor(
Expand Down Expand Up @@ -111,13 +109,13 @@ contract Irm is IIrm {
int256 errDelta = err - marketIrm[id].prevErr;

// Safe "unchecked" cast because LN_JUMP_FACTOR <= type(int256).max.
uint256 jumpMultiplier = MathLib.wExp12(errDelta.wMulDown(int256(LN_JUMP_FACTOR)));
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.wExp12(linearVariation);
uint256 variationMultiplier = MathLib.wExp(linearVariation);

// newBorrowRate = prevBorrowRate * jumpMultiplier * variationMultiplier.
uint256 borrowRateAfterJump = marketIrm[id].prevBorrowRate.wMulDown(jumpMultiplier);
Expand Down
2 changes: 2 additions & 0 deletions src/irm/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ pragma solidity ^0.8.0;
library ErrorsLib {
string internal constant MAX_INT128_EXCEEDED = "max int128 exceeded";
string internal constant INPUT_TOO_LARGE = "input too large";
string internal constant WEXP_UNDERFLOW = "wExp underflow";
string internal constant WEXP_OVERFLOW = "wExp overflow";
string internal constant NOT_MORPHO = "not Morpho";
}
43 changes: 27 additions & 16 deletions src/irm/libraries/MathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.0;

import {WAD, MathLib as MorphoMathLib} from "morpho-blue/libraries/MathLib.sol";
import {ErrorsLib} from "./ErrorsLib.sol";

int256 constant WAD_INT = int256(WAD);

Expand All @@ -11,23 +12,33 @@ library MathLib {
using {wDivDown} for int256;
using {wMulDown} for int256;

/// @dev 12th-order Taylor polynomial of e^x, for x around 0.
/// @dev The input is limited to a range between -3 and 3.
/// @dev The approximation error is less than 1% between -3 and 3.
function wExp12(int256 x) internal pure returns (uint256) {
x = x >= -3 * WAD_INT ? x : -3 * WAD_INT;
x = x <= 3 * WAD_INT ? x : 3 * WAD_INT;

// `N` should be even otherwise the result can be negative.
int256 N = 12;
int256 res = WAD_INT;
int256 monomial = WAD_INT;
for (int256 k = 1; k <= N; k++) {
monomial = monomial.wMulDown(x) / k;
res += monomial;
/// @dev ln(2).
int256 private constant LN2_INT = 0.693147180559945309 ether;

/// @dev Returns an approximation of exp.
function wExp(int256 x) internal pure returns (uint256) {
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);

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

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

// Return e^x = 2^q * e^r.
if (q >= 0) return expR << uint256(q);
else return expR >> uint256(-q);
}
// Safe "unchecked" cast because `N` is even.
return uint256(res);
}

function wMulDown(int256 a, int256 b) internal pure returns (int256) {
Expand Down
4 changes: 2 additions & 2 deletions test/irm/IrmTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ contract IrmTest is Test {
int256 errDelta = err - prevErr;
uint256 elapsed = block.timestamp - market1.lastUpdate;

uint256 jumpMultiplier = MathLib.wExp12(errDelta.wMulDown(int256(LN2)));
uint256 jumpMultiplier = MathLib.wExp(errDelta.wMulDown(int256(LN2)));
int256 speed = int256(SPEED_FACTOR).wMulDown(err);
uint256 variationMultiplier = MathLib.wExp12(speed * int256(elapsed));
uint256 variationMultiplier = MathLib.wExp(speed * int256(elapsed));
uint256 expectedBorrowRateAfterJump = INITIAL_RATE.wMulDown(jumpMultiplier);
uint256 expectedNewBorrowRate = INITIAL_RATE.wMulDown(jumpMultiplier).wMulDown(variationMultiplier);

Expand Down
50 changes: 24 additions & 26 deletions test/irm/MathLibTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,39 @@
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "solmate/utils/SignedWadMath.sol";
import "../../src/irm/libraries/MathLib.sol";
import "../../src/irm/libraries/ErrorsLib.sol";

contract MathLibTest is Test {
using MathLib for uint128;
using MathLib for uint256;

function testWExp() public {
assertEq(MathLib.wExp12(-5 ether), MathLib.wExp12(-3 ether));
assertApproxEqRel(MathLib.wExp12(-3 ether), 0.04978706836 ether, 0.005 ether);
assertApproxEqRel(MathLib.wExp12(-2 ether), 0.13533528323 ether, 0.00001 ether);
assertApproxEqRel(MathLib.wExp12(-1 ether), 0.36787944117 ether, 0.00000001 ether);
assertEq(MathLib.wExp12(0 ether), 1.0 ether);
assertApproxEqRel(MathLib.wExp12(1 ether), 2.71828182846 ether, 0.00000001 ether);
assertApproxEqRel(MathLib.wExp12(2 ether), 7.38905609893 ether, 0.00001 ether);
assertApproxEqRel(MathLib.wExp12(3 ether), 20.0855369232 ether, 0.001 ether);
assertEq(MathLib.wExp12(5 ether), MathLib.wExp12(3 ether));
}
int256 private constant LN2_INT = 0.693147180559945309 ether;

function testWExp(int256 x) public {
x = bound(x, -3 ether, 3 ether);
assertGe(int256(MathLib.wExp12(x)), int256(WAD) + x);
if (x < 0) assertLe(MathLib.wExp12(x), WAD);
assertApproxEqRel(MathLib.wExp12(x), wExpRef(x), 0.01 ether);
// 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);
}

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

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));
assertEq(MathLib.wExp(x), 0);
}
}

function wExpRef(int256 x) pure returns (uint256) {
// `N` should be even otherwise the result can be negative.
int256 N = 64;
int256 res = WAD_INT;
int256 monomial = WAD_INT;
for (int256 k = 1; k <= N; k++) {
monomial = monomial * x / WAD_INT / k;
res += monomial;
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);
}
// Safe "unchecked" cast because `N` is even.
return uint256(res);
}