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

IRM refactor #54

Merged
merged 122 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
2675073
feat: refactor curve abstraction
MathisGD Oct 27, 2023
1fdf81a
Merge remote-tracking branch 'origin/main' into refactor/curve
MathisGD Oct 27, 2023
33ea93d
test: fix some tests
MathisGD Oct 28, 2023
78a4144
chore: fmt
MathisGD Oct 28, 2023
a46068f
test: passing
MathisGD Oct 28, 2023
6b78b21
chore: fmt
MathisGD Oct 28, 2023
6cc6a4c
test: improve testing
MathisGD Oct 29, 2023
6dff847
feat: bound base rate
MathisGD Oct 29, 2023
10799a8
feat: remove the exponential aspect of the curve
MathisGD Oct 30, 2023
a437d35
style: new wording
MathisGD Oct 30, 2023
95a9ef0
style: contract name
MathisGD Oct 30, 2023
180a426
chore: fmt
MathisGD Oct 30, 2023
190834c
chore: update submodules
MathisGD Oct 30, 2023
2747975
chore: revert file name change
MathisGD Oct 30, 2023
e061946
chore: revert file name change
MathisGD Oct 30, 2023
d083e58
refactor(irm): compile without IR
Rubilmax Oct 31, 2023
c68cb45
fix: bound base rate
MathisGD Oct 31, 2023
89b0133
Merge branch 'refactor/curve' into refactor/build-profile
MathisGD Oct 31, 2023
a0f0539
Merge pull request #57 from morpho-labs/refactor/build-profile
MathisGD Oct 31, 2023
60e588a
fix: check morpho zero address
MathisGD Oct 31, 2023
89a1fdc
test: improve testing
MathisGD Oct 31, 2023
8eb5285
style: base rate naming
MathisGD Oct 31, 2023
143c55c
chore: fmt
MathisGD Oct 31, 2023
9de0936
fix: minor issues
MathisGD Oct 31, 2023
6e99116
fix: minor improvements
MathisGD Nov 2, 2023
aaaa332
fix: various fixes
MathisGD Nov 4, 2023
4ee7cb9
refactor: separate first interaction case
MathisGD Nov 4, 2023
76ca930
docs: add comment on constant speed
MathisGD Nov 4, 2023
c123cfd
chore: space
MathisGD Nov 4, 2023
c3154c9
refactor(math): revert to int256
Rubilmax Nov 6, 2023
fff19be
refactor(irm): expect uint256 inputs
Rubilmax Nov 6, 2023
80e075e
chore: space
MathisGD Nov 4, 2023
1eff87c
docs: fix incorrect comment
MerlinEgalite Nov 8, 2023
50a2612
fix: return a capped value instead of overflowing
MerlinEgalite Nov 9, 2023
69fba05
docs: correct comment for tests as well
MerlinEgalite Nov 9, 2023
70f15f7
style: format
MerlinEgalite Nov 9, 2023
a2a4042
refactor: apply naming suggestion
MerlinEgalite Nov 9, 2023
54b23a5
refactor: use steeringCoeff
MathisGD Nov 9, 2023
469fd90
Merge remote-tracking branch 'origin/refactor/curve' into refactor/curve
MathisGD Nov 9, 2023
15b0f33
chore: fmt
MathisGD Nov 9, 2023
3419458
docs: comment adjustment speed non negative
MathisGD Nov 9, 2023
926cc45
Merge branch 'refactor/curve' into refactor/w-mul-down
MathisGD Nov 9, 2023
bcc4cf1
chore: fmt
MathisGD Nov 9, 2023
9614561
Merge pull request #60 from morpho-labs/refactor/w-mul-down
MathisGD Nov 9, 2023
c82fc3d
docs: improve comment return zero
MathisGD Nov 9, 2023
bd0b5cd
Merge pull request #61 from morpho-labs/fix/incorrect-comment-36
MathisGD Nov 9, 2023
222b04e
docs: minor improvements
MathisGD Nov 10, 2023
939d3d0
fix(wexp): prevent reverts
Rubilmax Nov 10, 2023
7f09d96
Merge branch 'refactor/curve' of github.com:morpho-labs/morpho-blue-p…
Rubilmax Nov 10, 2023
3c23f2a
test(foundry): fix invariant tests
Rubilmax Nov 10, 2023
3129b2b
refactor: use only int256
MathisGD Nov 10, 2023
ecad414
fix: minor improvements
MathisGD Nov 10, 2023
e26a72a
docs: non negative result
MathisGD Nov 11, 2023
370b43e
docs: avgBorrowRate non zero
MathisGD Nov 11, 2023
de8c78f
docs: document block.timestamp safe unchecked
MathisGD Nov 11, 2023
948db52
fix(wExp): update values
Rubilmax Nov 11, 2023
6717428
refactor: store int
MathisGD Nov 11, 2023
db9541b
docs: minor improvements
MathisGD Nov 11, 2023
5738df9
fix(wexp): raise upper bound to ln(max / 1e36)
Rubilmax Nov 12, 2023
ead8b28
Merge branch 'fix/exp-overflow-35' of github.com:morpho-labs/morpho-b…
Rubilmax Nov 13, 2023
f9a1bf7
test(irm): revert invariants
Rubilmax Nov 13, 2023
dee15a6
chore: update blue
MathisGD Nov 13, 2023
b209956
Merge remote-tracking branch 'origin/main' into refactor/curve
MathisGD Nov 13, 2023
8b66f45
refactor: curve function
MathisGD Nov 13, 2023
2e9953b
test: improve testing
MathisGD Nov 13, 2023
ea7a95e
Merge pull request #75 from morpho-labs/refactor/curve-function
MathisGD Nov 13, 2023
145ff39
Merge pull request #74 from morpho-labs/chore/update-blue
MathisGD Nov 13, 2023
af82fe2
docs: minor improvement
MathisGD Nov 13, 2023
98c0d61
docs: minor improvements
MathisGD Nov 13, 2023
b14c054
Merge pull request #68 from morpho-labs/test/invariant-no-revert
MathisGD Nov 13, 2023
6733f35
Merge branch 'refactor/curve' into refactor/int
MathisGD Nov 13, 2023
40f6ac0
Merge remote-tracking branch 'origin/refactor/curve' into refactor/int
MathisGD Nov 13, 2023
03b9684
Merge remote-tracking branch 'origin/refactor/curve' into fix/exp-ove…
MathisGD Nov 13, 2023
3d5e144
chore: fmt
MathisGD Nov 13, 2023
50ce806
test: add more tests
MathisGD Nov 14, 2023
9f80268
chore: update libs
MathisGD Nov 14, 2023
6ebacf0
Merge pull request #62 from morpho-labs/fix/exp-overflow-35
MathisGD Nov 14, 2023
447a232
Merge branch 'refactor/curve' into refactor/int
MathisGD Nov 14, 2023
a345815
fix: upper value
MathisGD Nov 14, 2023
62937d2
test: add exp tests
MathisGD Nov 14, 2023
006384a
docs: minor improvements
MathisGD Nov 14, 2023
8483911
Merge pull request #69 from morpho-labs/refactor/int
MathisGD Nov 14, 2023
40bf4a7
test: add APY
MathisGD Nov 14, 2023
d4219ec
Merge remote-tracking branch 'origin/test/refactor-curve' into test/r…
MathisGD Nov 14, 2023
d239efe
Merge remote-tracking branch 'origin/refactor/curve' into test/refact…
MathisGD Nov 14, 2023
a1826c9
test: minor fix
MathisGD Nov 14, 2023
77ed9c1
Merge pull request #76 from morpho-labs/test/refactor-curve
MathisGD Nov 14, 2023
1c3bedf
feat: riemann avg
MathisGD Nov 15, 2023
6e49f0e
style: minor improvements
MathisGD Nov 15, 2023
8163f2a
Merge branch 'main' into chore/merge-main
MathisGD Nov 15, 2023
d68db06
Merge pull request #84 from morpho-org/chore/merge-main
MathisGD Nov 15, 2023
fd24cc2
Merge remote-tracking branch 'origin/refactor/curve' into feat/rieman…
MathisGD Nov 15, 2023
3ea4939
docs: document riemann avg
MathisGD Nov 15, 2023
268a4f4
perf: factorize div by N
MathisGD Nov 15, 2023
2ab1a43
docs: document N_STEPS
MathisGD Nov 15, 2023
770a6fc
docs: document riemann
MathisGD Nov 16, 2023
16124b8
style: renamings
MathisGD Nov 16, 2023
5689b78
chore: fmt
MathisGD Nov 16, 2023
f1c618f
perf: use endRateAtTarget in riemann
MathisGD Nov 16, 2023
a6099fc
docs: minor improvements
MathisGD Nov 16, 2023
e0167ec
feat: left/right riemann
MathisGD Nov 16, 2023
91039d9
style: harmonize naming
MathisGD Nov 16, 2023
09c05e3
docs: remove we
MathisGD Nov 16, 2023
6682ad0
docs: minor fix
MathisGD Nov 16, 2023
500a957
feat: return early
MathisGD Nov 16, 2023
da8062f
chore: rename contract
MathisGD Nov 16, 2023
8fbaffc
docs: minor fix
MathisGD Nov 16, 2023
af5e27e
Merge branch 'feat/riemann-avg' into feat/return-early-linearAdaptati…
MathisGD Nov 16, 2023
1387436
docs: format doc
MathisGD Nov 16, 2023
0974005
Merge pull request #88 from morpho-org/feat/return-early-linearAdapta…
MathisGD Nov 16, 2023
52b9307
refactor: riemann
MathisGD Nov 16, 2023
9dac5b9
feat: trapezoidal riemann (n=2)
MathisGD Nov 16, 2023
8b81fdc
Merge pull request #89 from morpho-org/refactor/proposal-1
MathisGD Nov 16, 2023
c1633da
docs
MathisGD Nov 16, 2023
f75a09e
chore: fmt
MathisGD Nov 16, 2023
0531b5c
Merge branch 'feat/riemann-avg' into feat/trapezoidal
MathisGD Nov 16, 2023
e46719d
docs: more concise comments for trapeze N=2
QGarchery Nov 16, 2023
7667991
fix: missing bracket
QGarchery Nov 16, 2023
97794c6
Merge pull request #95 from morpho-org/docs/trapezoidal
MathisGD Nov 16, 2023
089da66
Merge pull request #91 from morpho-org/feat/trapezoidal
MerlinEgalite Nov 16, 2023
f0ebd8b
test: larger approx
MathisGD Nov 16, 2023
4637e31
Merge pull request #82 from morpho-org/feat/riemann-avg
MerlinEgalite Nov 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ libs = ["lib"]
[profile.default.fuzz]
runs = 4096

[profile.default.invariant]
fail_on_revert = true

[profile.default.fmt]
wrap_comments = true

Expand All @@ -20,5 +23,13 @@ script = "/dev/null"
[profile.test]
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
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
213 changes: 121 additions & 92 deletions src/SpeedJumpIrm.sol
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
Rubilmax marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,93 @@ 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 [email protected]
/// @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;

/* 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).
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved
/// @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;
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -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 {
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
// 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);
}
}
12 changes: 6 additions & 6 deletions src/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
36 changes: 22 additions & 14 deletions src/libraries/MathLib.sol
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/UtilsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
}
}
Loading