From 1c3bedf4526d0d90b40f1bd5286c9cb7985cddb1 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 15 Nov 2023 16:20:02 +0100 Subject: [PATCH 01/25] feat: riemann avg --- src/SpeedJumpIrm.sol | 31 +++++++++++++------------------ test/SpeedJumpIrmTest.sol | 36 ++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index f831a1f5..cc6d494a 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -10,6 +10,8 @@ import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib. import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; +int256 constant numberOfSteps = 4; + /// @title AdaptativeCurveIrm /// @author Morpho Labs /// @custom:contact security@morpho.org @@ -120,8 +122,8 @@ contract AdaptativeCurveIrm is IIrm { int256 startRateAtTarget = rateAtTarget[id]; - // First interaction. if (startRateAtTarget == 0) { + // First interaction. return (uint256(_curve(INITIAL_RATE_AT_TARGET, err)), INITIAL_RATE_AT_TARGET); } else { // Note that the speed is assumed constant between two interactions, but in theory it increases because of @@ -132,26 +134,19 @@ contract AdaptativeCurveIrm is IIrm { // Safe "unchecked" cast because block.timestamp - market.lastUpdate <= block.timestamp <= type(int256).max. int256 elapsed = int256(block.timestamp - market.lastUpdate); int256 linearAdaptation = speed * elapsed; - int256 adaptationMultiplier = MathLib.wExp(linearAdaptation); // endRateAtTarget is bounded between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET. int256 endRateAtTarget = - startRateAtTarget.wMulDown(adaptationMultiplier).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); - int256 endBorrowRate = _curve(endRateAtTarget, err); - - // Then we compute the average rate over the period. - // Note that startBorrowRate is defined in the computations below. - // avgBorrowRate = 1 / elapsed * ∫ startBorrowRate * exp(speed * t) dt between 0 and elapsed - // = startBorrowRate * (exp(linearAdaptation) - 1) / linearAdaptation - // = (endBorrowRate - startBorrowRate) / linearAdaptation - // And for linearAdaptation around zero: avgBorrowRate ~ startBorrowRate = endBorrowRate. - // Also, when it is the first interaction (rateAtTarget = 0). - int256 avgBorrowRate; - if (linearAdaptation == 0) { - avgBorrowRate = endBorrowRate; - } else { - int256 startBorrowRate = _curve(startRateAtTarget, err); - avgBorrowRate = (endBorrowRate - startBorrowRate).wDivDown(linearAdaptation); + startRateAtTarget.wMulDown(MathLib.wExp(linearAdaptation)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); + + // Then we compute the average rate over the period, with a Riemann sum. + int256 averageRateAtTarget; + int256 step = linearAdaptation / numberOfSteps; + for (int256 k = 1; k <= numberOfSteps; k++) { + averageRateAtTarget += startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound( + MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET + ) / numberOfSteps; } + int256 avgBorrowRate = _curve(averageRateAtTarget, err); // avgBorrowRate is non negative because: // - endBorrowRate >= 0 because endRateAtTarget >= MIN_RATE_AT_TARGET. diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index 57d84e24..bcb6604f 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -76,22 +76,22 @@ contract AdaptativeCurveIrmTest is Test { market.totalBorrowAssets = 1 ether; market.totalSupplyAssets = 1 ether; - market.lastUpdate = uint128(block.timestamp - 30 days); + market.lastUpdate = uint128(block.timestamp - 5 days); - // (exp((50/365)*30) ~= 61. + // (exp((50/365)*5) ~= 1.9836. assertApproxEqRel( irm.borrowRateView(marketParams, market), - uint256((INITIAL_RATE_AT_TARGET * 4).wMulDown((61 ether - 1 ether) * WAD / (ADJUSTMENT_SPEED * 30 days))), + uint256((INITIAL_RATE_AT_TARGET * 4).wMulDown((1.9836 ether - 1 ether) * WAD / (ADJUSTMENT_SPEED * 5 days))), 0.1 ether ); - // The average value of exp((50/365)*30) between 0 and 30 is approx. 14.58. + // The average value of exp((50/365)*x) between 0 and 5 is approx. 1.4361. assertApproxEqRel( irm.borrowRateView(marketParams, market), - uint256((INITIAL_RATE_AT_TARGET * 4).wMulDown(14.58 ether)), + uint256((INITIAL_RATE_AT_TARGET * 4).wMulDown(1.4361 ether)), 0.1 ether ); - // Expected rate: 58%. - assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.58 ether) / 365 days, 0.1 ether); + // Expected rate: 5.744%. + assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.05744 ether) / 365 days, 0.1 ether); } function testRateAfterUtilizationZero() public { @@ -101,24 +101,24 @@ contract AdaptativeCurveIrmTest is Test { market.totalBorrowAssets = 0 ether; market.totalSupplyAssets = 1 ether; - market.lastUpdate = uint128(block.timestamp - 30 days); + market.lastUpdate = uint128(block.timestamp - 5 days); - // (exp((-50/365)*30) ~= 0.016. + // (exp((-50/365)*5) ~= 0.5041. assertApproxEqRel( irm.borrowRateView(marketParams, market), uint256( - (INITIAL_RATE_AT_TARGET / 4).wMulDown((0.016 ether - 1 ether) * WAD / (-ADJUSTMENT_SPEED * 30 days)) + (INITIAL_RATE_AT_TARGET / 4).wMulDown((0.5041 ether - 1 ether) * WAD / (-ADJUSTMENT_SPEED * 5 days)) ), 0.1 ether ); - // The average value of exp((-50/365*30)) between 0 and 30 is approx. 0.239. + // The average value of exp((-50/365*x)) between 0 and 5 is approx. 0.7240. assertApproxEqRel( irm.borrowRateView(marketParams, market), - uint256((INITIAL_RATE_AT_TARGET / 4).wMulDown(0.23 ether)), + uint256((INITIAL_RATE_AT_TARGET / 4).wMulDown(0.7240 ether)), 0.1 ether ); - // Expected rate: 0.057%. - assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.00057 ether) / 365 days, 0.1 ether); + // Expected rate: 0.181%. + assertApproxEqRel(irm.borrowRateView(marketParams, market), uint256(0.00181 ether) / 365 days, 0.1 ether); } function testFirstBorrowRate(Market memory market) public { @@ -163,7 +163,7 @@ contract AdaptativeCurveIrmTest is Test { vm.assume(market1.totalBorrowAssets > 0); vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); - market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); + market1.lastUpdate = uint128(bound(market1.lastUpdate, block.timestamp - 5 days, block.timestamp - 1)); int256 expectedRateAtTarget = _expectedRateAtTarget(marketParams.id(), market1); uint256 expectedAvgRate = _expectedAvgRate(marketParams.id(), market1); @@ -172,7 +172,7 @@ contract AdaptativeCurveIrmTest is Test { uint256 borrowRate = irm.borrowRate(marketParams, market1); assertEq(borrowRateView, borrowRate, "borrowRateView"); - assertApproxEqRel(borrowRate, expectedAvgRate, 0.01 ether, "avgBorrowRate"); + assertApproxEqRel(borrowRate, expectedAvgRate, 0.1 ether, "avgBorrowRate"); assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); } @@ -203,7 +203,7 @@ contract AdaptativeCurveIrmTest is Test { market1.totalBorrowAssets = market0.totalBorrowAssets; market1.totalSupplyAssets = market0.totalSupplyAssets; - market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); + market1.lastUpdate = uint128(bound(market1.lastUpdate, block.timestamp - 5 days, block.timestamp - 1)); int256 expectedRateAtTarget = _expectedRateAtTarget(marketParams.id(), market1); uint256 expectedAvgRate = _expectedAvgRate(marketParams.id(), market1); @@ -212,7 +212,7 @@ contract AdaptativeCurveIrmTest is Test { uint256 borrowRate = irm.borrowRate(marketParams, market1); assertEq(borrowRateView, borrowRate, "borrowRateView"); - assertApproxEqRel(borrowRate, expectedAvgRate, 0.01 ether, "avgBorrowRate"); + assertApproxEqRel(borrowRate, expectedAvgRate, 0.1 ether, "avgBorrowRate"); assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); } From 6e49f0e5d7291ec155c61ed92de7992337c9e1b7 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 15 Nov 2023 16:33:16 +0100 Subject: [PATCH 02/25] style: minor improvements --- src/SpeedJumpIrm.sol | 17 +++++++---------- test/SpeedJumpIrmTest.sol | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index cc6d494a..b3a1c326 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -10,7 +10,7 @@ import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib. import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; -int256 constant numberOfSteps = 4; +int256 constant N_STEPS = 4; /// @title AdaptativeCurveIrm /// @author Morpho Labs @@ -129,29 +129,26 @@ contract AdaptativeCurveIrm is IIrm { // 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; + // endRateAtTarget is bounded between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET. int256 endRateAtTarget = startRateAtTarget.wMulDown(MathLib.wExp(linearAdaptation)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); // Then we compute the average rate over the period, with a Riemann sum. int256 averageRateAtTarget; - int256 step = linearAdaptation / numberOfSteps; - for (int256 k = 1; k <= numberOfSteps; k++) { + int256 step = linearAdaptation / N_STEPS; + for (int256 k = 1; k <= N_STEPS; k++) { averageRateAtTarget += startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound( MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET - ) / numberOfSteps; + ) / N_STEPS; } - int256 avgBorrowRate = _curve(averageRateAtTarget, err); - // avgBorrowRate is non negative because: - // - endBorrowRate >= 0 because endRateAtTarget >= MIN_RATE_AT_TARGET. - // - linearAdaptation < 0 <=> adaptationMultiplier <= 1 <=> endBorrowRate <= startBorrowRate. - return (uint256(avgBorrowRate), endRateAtTarget); + // avgBorrowRate is non negative because averageRateAtTarget is non negative. + return (uint256(_curve(averageRateAtTarget, err)), endRateAtTarget); } } diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index bcb6604f..bea89ca9 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -114,7 +114,7 @@ contract AdaptativeCurveIrmTest is Test { // The average value of exp((-50/365*x)) between 0 and 5 is approx. 0.7240. assertApproxEqRel( irm.borrowRateView(marketParams, market), - uint256((INITIAL_RATE_AT_TARGET / 4).wMulDown(0.7240 ether)), + uint256((INITIAL_RATE_AT_TARGET / 4).wMulDown(0.724 ether)), 0.1 ether ); // Expected rate: 0.181%. From 3ea4939a388d7d57b7f1bf647286087c07bbd3d0 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 15 Nov 2023 17:22:20 +0100 Subject: [PATCH 03/25] docs: document riemann avg --- src/SpeedJumpIrm.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index b3a1c326..19a24d2c 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -139,6 +139,8 @@ contract AdaptativeCurveIrm is IIrm { startRateAtTarget.wMulDown(MathLib.wExp(linearAdaptation)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); // Then we compute the average rate over the period, with a Riemann sum. + // We omit the multiplication by the rectangle length because we would divide everything by the total length + // at the end, because we want to compute the average and not the integral. int256 averageRateAtTarget; int256 step = linearAdaptation / N_STEPS; for (int256 k = 1; k <= N_STEPS; k++) { From 268a4f474231d72f4479c14485fa7b682b9f1842 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 15 Nov 2023 17:28:29 +0100 Subject: [PATCH 04/25] perf: factorize div by N --- src/SpeedJumpIrm.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 19a24d2c..6fc8cc43 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -141,13 +141,12 @@ contract AdaptativeCurveIrm is IIrm { // Then we compute the average rate over the period, with a Riemann sum. // We omit the multiplication by the rectangle length because we would divide everything by the total length // at the end, because we want to compute the average and not the integral. - int256 averageRateAtTarget; + int256 sum; int256 step = linearAdaptation / N_STEPS; for (int256 k = 1; k <= N_STEPS; k++) { - averageRateAtTarget += startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound( - MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET - ) / N_STEPS; + sum += startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); } + int256 averageRateAtTarget = sum / N_STEPS; // avgBorrowRate is non negative because averageRateAtTarget is non negative. return (uint256(_curve(averageRateAtTarget, err)), endRateAtTarget); From 2ab1a4313b0c6dfed99a913fe988a8181d397c53 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Wed, 15 Nov 2023 19:49:17 +0100 Subject: [PATCH 05/25] docs: document N_STEPS --- src/SpeedJumpIrm.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 6fc8cc43..480e76b2 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -10,6 +10,8 @@ import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib. import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; +/// @dev Number of steps used in the Riemann sum. +/// @dev 4 steps allows to have a relative error below 30% for 15 days at err=1 or err=-1. int256 constant N_STEPS = 4; /// @title AdaptativeCurveIrm From 770a6fce5605f0dbbb0bb6bc1dcad5aa573f1724 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 10:24:22 +0100 Subject: [PATCH 06/25] docs: document riemann --- src/SpeedJumpIrm.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 480e76b2..8ac2f0e8 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -140,9 +140,14 @@ contract AdaptativeCurveIrm is IIrm { int256 endRateAtTarget = startRateAtTarget.wMulDown(MathLib.wExp(linearAdaptation)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); - // Then we compute the average rate over the period, with a Riemann sum. - // We omit the multiplication by the rectangle length because we would divide everything by the total length - // at the end, because we want to compute the average and not the integral. + // We want to approximate an average of the rate over the period [0, T]: + // avg = 1/T ∫_0^T curve(startRateAtTarget * exp(speed * x), err) dx + // We approximate the integral with a Riemann sum (steps of length T/N): + // avg ~= 1/T Σ_i=1^N curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N + // ~= Σ_i=1^N curve(startRateAtTarget * exp(linearVariation/N * i), err) / N + // curve is linear in startRateAtTarget, so: + // ~= curve(Σ_i=1^N startRateAtTarget * exp(linearVariation/N * i), err) / N + // ~= curve(Σ_i=1^N startRateAtTarget * exp(linearVariation/N * i) / N, err) int256 sum; int256 step = linearAdaptation / N_STEPS; for (int256 k = 1; k <= N_STEPS; k++) { From 16124b81887aa5b032149e89255946a64fc6c79d Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 10:42:08 +0100 Subject: [PATCH 07/25] style: renamings --- src/SpeedJumpIrm.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 8ac2f0e8..f43e416f 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -148,15 +148,15 @@ contract AdaptativeCurveIrm is IIrm { // curve is linear in startRateAtTarget, so: // ~= curve(Σ_i=1^N startRateAtTarget * exp(linearVariation/N * i), err) / N // ~= curve(Σ_i=1^N startRateAtTarget * exp(linearVariation/N * i) / N, err) - int256 sum; + int256 sumRateAtTarget; int256 step = linearAdaptation / N_STEPS; for (int256 k = 1; k <= N_STEPS; k++) { - sum += startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); + sumRateAtTarget += startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); } - int256 averageRateAtTarget = sum / N_STEPS; + int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; - // avgBorrowRate is non negative because averageRateAtTarget is non negative. - return (uint256(_curve(averageRateAtTarget, err)), endRateAtTarget); + // avgBorrowRate is non negative because avgRateAtTarget is non negative. + return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); } } From 5689b780faa58af462fc63d099327ad9b63c4ea1 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 10:42:55 +0100 Subject: [PATCH 08/25] chore: fmt --- src/SpeedJumpIrm.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index f43e416f..31e5fe30 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -151,7 +151,8 @@ contract AdaptativeCurveIrm is IIrm { int256 sumRateAtTarget; int256 step = linearAdaptation / N_STEPS; for (int256 k = 1; k <= N_STEPS; k++) { - sumRateAtTarget += startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); + sumRateAtTarget += + startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); } int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; From f1c618fde8094cb417c12abfad978d895882220e Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 11:01:06 +0100 Subject: [PATCH 09/25] perf: use endRateAtTarget in riemann --- src/SpeedJumpIrm.sol | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 31e5fe30..28e7df3e 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -112,7 +112,7 @@ contract AdaptativeCurveIrm is IIrm { return avgBorrowRate; } - /// @dev Returns avgBorrowRate and endRateAtTarget. + /// @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). @@ -136,10 +136,6 @@ contract AdaptativeCurveIrm is IIrm { int256 elapsed = int256(block.timestamp - market.lastUpdate); int256 linearAdaptation = speed * elapsed; - // endRateAtTarget is bounded between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET. - int256 endRateAtTarget = - startRateAtTarget.wMulDown(MathLib.wExp(linearAdaptation)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); - // We want to approximate an average of the rate over the period [0, T]: // avg = 1/T ∫_0^T curve(startRateAtTarget * exp(speed * x), err) dx // We approximate the integral with a Riemann sum (steps of length T/N): @@ -150,13 +146,15 @@ contract AdaptativeCurveIrm is IIrm { // ~= curve(Σ_i=1^N startRateAtTarget * exp(linearVariation/N * i) / N, err) int256 sumRateAtTarget; int256 step = linearAdaptation / N_STEPS; - for (int256 k = 1; k <= N_STEPS; k++) { - sumRateAtTarget += - startRateAtTarget.wMulDown(MathLib.wExp(step * k)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); + // Stops after N_STEPS - 1, the last one is done with endRateAtTarget. + for (int256 k = 1; k < N_STEPS; k++) { + sumRateAtTarget += _newRateAtTarget(startRateAtTarget, step * k); } + int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); + sumRateAtTarget += endRateAtTarget; int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; - // avgBorrowRate is non negative because avgRateAtTarget is non negative. + // avgRate is non negative because avgRateAtTarget is non negative. return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); } } @@ -171,4 +169,8 @@ contract AdaptativeCurveIrm is IIrm { // Non negative because if err < 0, coeff <= 1. return (coeff.wMulDown(err) + WAD).wMulDown(int256(_rateAtTarget)); } + + function _newRateAtTarget(int256 startRateAtTarget, int256 linearAdaptation) private pure returns (int256) { + return startRateAtTarget.wMulDown(MathLib.wExp(linearAdaptation)).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET); + } } From a6099fc3444e62d77132939d616f6e2f74d5f0e7 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 11:17:01 +0100 Subject: [PATCH 10/25] docs: minor improvements --- src/SpeedJumpIrm.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 28e7df3e..20dd4220 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -126,6 +126,7 @@ contract AdaptativeCurveIrm is IIrm { if (startRateAtTarget == 0) { // First interaction. + // Safe "unchecked cast" because INITIAL_RATE_AT_TARGET >= 0. return (uint256(_curve(INITIAL_RATE_AT_TARGET, err)), INITIAL_RATE_AT_TARGET); } else { // Note that the speed is assumed constant between two interactions, but in theory it increases because of @@ -154,7 +155,7 @@ contract AdaptativeCurveIrm is IIrm { sumRateAtTarget += endRateAtTarget; int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; - // avgRate is non negative because avgRateAtTarget is non negative. + // Safe "unchecked cast" because avgRateAtTarget >= 0. return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); } } @@ -166,11 +167,14 @@ contract AdaptativeCurveIrm is IIrm { 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 because if err < 0, coeff <= 1. + // Non negative if _rateAtTarget >= 0 because if err < 0, coeff <= 1. return (coeff.wMulDown(err) + WAD).wMulDown(int256(_rateAtTarget)); } + /// @dev Returns the new rate at target, for a given `startRateAtTarget` and a given `linearAdaptation`. + /// The formula is the following: newRAT = max(min(startRAT * exp(linearAdaptation), MAX_RAT), MIN_RAT). 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); } } From e0167ec9334491a54c69e708b551248508b9209b Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 12:40:14 +0100 Subject: [PATCH 11/25] feat: left/right riemann --- src/SpeedJumpIrm.sol | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 20dd4220..87513b63 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -126,7 +126,7 @@ contract AdaptativeCurveIrm is IIrm { if (startRateAtTarget == 0) { // First interaction. - // Safe "unchecked cast" because INITIAL_RATE_AT_TARGET >= 0. + // Safe "unchecked" cast because INITIAL_RATE_AT_TARGET >= 0. return (uint256(_curve(INITIAL_RATE_AT_TARGET, err)), INITIAL_RATE_AT_TARGET); } else { // Note that the speed is assumed constant between two interactions, but in theory it increases because of @@ -139,23 +139,26 @@ contract AdaptativeCurveIrm is IIrm { // We want to approximate an average of the rate over the period [0, T]: // avg = 1/T ∫_0^T curve(startRateAtTarget * exp(speed * x), err) dx - // We approximate the integral with a Riemann sum (steps of length T/N): - // avg ~= 1/T Σ_i=1^N curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N - // ~= Σ_i=1^N curve(startRateAtTarget * exp(linearVariation/N * i), err) / N + // We approximate the integral with a Riemann sum (steps of length T/N). We want to underestimate the rate, + // which means doing a left Riemann (a=0, b=N-1) when the rate goes up (err>0) and a right Riemann (a=1, + // b=N) when the rate goes down (err<0). + // avg ~= 1/T Σ_i=a^b curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N + // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearVariation/N * i), err) / N // curve is linear in startRateAtTarget, so: - // ~= curve(Σ_i=1^N startRateAtTarget * exp(linearVariation/N * i), err) / N - // ~= curve(Σ_i=1^N startRateAtTarget * exp(linearVariation/N * i) / N, err) + // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i), err) / N + // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i) / N, err) int256 sumRateAtTarget; int256 step = linearAdaptation / N_STEPS; - // Stops after N_STEPS - 1, the last one is done with endRateAtTarget. + // Compute the terms 1 to N_STEPS - 1. for (int256 k = 1; k < N_STEPS; k++) { sumRateAtTarget += _newRateAtTarget(startRateAtTarget, step * k); } int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); - sumRateAtTarget += endRateAtTarget; + // Add the term 0 of N_STEPS depending of if we do a left or right Riemann. + sumRateAtTarget += err < 0 ? endRateAtTarget : startRateAtTarget; int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; - // Safe "unchecked cast" because avgRateAtTarget >= 0. + // Safe "unchecked" cast because avgRateAtTarget >= 0. return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); } } @@ -172,7 +175,7 @@ contract AdaptativeCurveIrm is IIrm { } /// @dev Returns the new rate at target, for a given `startRateAtTarget` and a given `linearAdaptation`. - /// The formula is the following: newRAT = max(min(startRAT * exp(linearAdaptation), MAX_RAT), MIN_RAT). + /// 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); From 91039d984085fc7dd9696fac400b609865a47181 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 12:46:15 +0100 Subject: [PATCH 12/25] style: harmonize naming --- src/SpeedJumpIrm.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 87513b63..5f6287d5 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -92,8 +92,8 @@ contract AdaptativeCurveIrm is IIrm { /// @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 @@ -102,14 +102,14 @@ contract AdaptativeCurveIrm is IIrm { Id id = marketParams.id(); - (uint256 avgBorrowRate, int256 endRateAtTarget) = _borrowRate(id, market); + (uint256 avgRate, int256 endRateAtTarget) = _borrowRate(id, market); rateAtTarget[id] = endRateAtTarget; // Safe "unchecked" because endRateAtTarget >= 0. - emit BorrowRateUpdate(id, avgBorrowRate, uint256(endRateAtTarget)); + emit BorrowRateUpdate(id, avgRate, uint256(endRateAtTarget)); - return avgBorrowRate; + return avgRate; } /// @dev Returns avgRate and endRateAtTarget. From 09c05e3ff7ba1d559dd6e59145d2e2d392b3b527 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 14:28:23 +0100 Subject: [PATCH 13/25] docs: remove we --- src/SpeedJumpIrm.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 5f6287d5..8892e41b 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -137,11 +137,11 @@ contract AdaptativeCurveIrm is IIrm { int256 elapsed = int256(block.timestamp - market.lastUpdate); int256 linearAdaptation = speed * elapsed; - // We want to approximate an average of the rate over the period [0, T]: + // Formula of the average rate that should be returned to Morpho Blue: // avg = 1/T ∫_0^T curve(startRateAtTarget * exp(speed * x), err) dx - // We approximate the integral with a Riemann sum (steps of length T/N). We want to underestimate the rate, - // which means doing a left Riemann (a=0, b=N-1) when the rate goes up (err>0) and a right Riemann (a=1, - // b=N) when the rate goes down (err<0). + // The integral is approximated with a Riemann sum (steps of length T/N). To underestimate the rate, a left + // Riemann (a=0, b=N-1) is done when the rate goes up (err>0) and a right Riemann (a=1, b=N) is done when + // the rate goes down (err<0). // avg ~= 1/T Σ_i=a^b curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearVariation/N * i), err) / N // curve is linear in startRateAtTarget, so: @@ -154,7 +154,7 @@ contract AdaptativeCurveIrm is IIrm { sumRateAtTarget += _newRateAtTarget(startRateAtTarget, step * k); } int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); - // Add the term 0 of N_STEPS depending of if we do a left or right Riemann. + // Add the term 0 for a left Riemann or the term N_STEPS for a right Riemann. sumRateAtTarget += err < 0 ? endRateAtTarget : startRateAtTarget; int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; From 6682ad0ecb0e05e77b1ffd70451ee5993e5083b3 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 14:30:04 +0100 Subject: [PATCH 14/25] docs: minor fix --- src/SpeedJumpIrm.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 8892e41b..dd28c82b 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -106,7 +106,7 @@ contract AdaptativeCurveIrm is IIrm { rateAtTarget[id] = endRateAtTarget; - // Safe "unchecked" because endRateAtTarget >= 0. + // Safe "unchecked" cast because endRateAtTarget >= 0. emit BorrowRateUpdate(id, avgRate, uint256(endRateAtTarget)); return avgRate; From 500a95710130b121f263031cefc4719d326f940a Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 15:19:05 +0100 Subject: [PATCH 15/25] feat: return early --- src/SpeedJumpIrm.sol | 51 +++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index dd28c82b..3ed52d6f 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -137,29 +137,36 @@ contract AdaptativeCurveIrm is IIrm { int256 elapsed = int256(block.timestamp - market.lastUpdate); int256 linearAdaptation = speed * elapsed; - // 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 a Riemann sum (steps of length T/N). To underestimate the rate, a left - // Riemann (a=0, b=N-1) is done when the rate goes up (err>0) and a right Riemann (a=1, b=N) is done when - // the rate goes down (err<0). - // avg ~= 1/T Σ_i=a^b curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N - // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearVariation/N * i), err) / N - // curve is linear in startRateAtTarget, so: - // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i), err) / N - // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i) / N, err) - int256 sumRateAtTarget; - int256 step = linearAdaptation / N_STEPS; - // Compute the terms 1 to N_STEPS - 1. - for (int256 k = 1; k < N_STEPS; k++) { - sumRateAtTarget += _newRateAtTarget(startRateAtTarget, step * k); + if (linearAdaptation == 0) { + // If linearAdaptation == 0, avgRateAtTarget = endRateAtTarget = startRateAtTarget; + return (uint256(_curve(startRateAtTarget, err)), 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 a Riemann sum (steps of length T/N). To underestimate the rate, a + // left + // Riemann (a=0, b=N-1) is done when the rate goes up (err>0) and a right Riemann (a=1, b=N) is done + // when + // the rate goes down (err<0). + // avg ~= 1/T Σ_i=a^b curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N + // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearVariation/N * i), err) / N + // curve is linear in startRateAtTarget, so: + // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i), err) / N + // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i) / N, err) + int256 sumRateAtTarget; + int256 step = linearAdaptation / N_STEPS; + // Compute the terms 1 to N_STEPS - 1. + for (int256 k = 1; k < N_STEPS; k++) { + sumRateAtTarget += _newRateAtTarget(startRateAtTarget, step * k); + } + int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); + // Add the term 0 for a left Riemann or the term N_STEPS for a right Riemann. + sumRateAtTarget += err < 0 ? endRateAtTarget : startRateAtTarget; + int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; + + // Safe "unchecked" cast because avgRateAtTarget >= 0. + return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); } - int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); - // Add the term 0 for a left Riemann or the term N_STEPS for a right Riemann. - sumRateAtTarget += err < 0 ? endRateAtTarget : startRateAtTarget; - int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; - - // Safe "unchecked" cast because avgRateAtTarget >= 0. - return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); } } From da8062f228e4a8b6b46553fc46bc53234cc172b7 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 15:24:04 +0100 Subject: [PATCH 16/25] chore: rename contract --- src/SpeedJumpIrm.sol | 4 ++-- test/MathLibTest.sol | 2 +- test/SpeedJumpIrmTest.sol | 16 +++++++++------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index dd28c82b..429e385f 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -14,10 +14,10 @@ import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib /// @dev 4 steps allows to have a relative error below 30% for 15 days at err=1 or err=-1. int256 constant N_STEPS = 4; -/// @title AdaptativeCurveIrm +/// @title AdaptiveCurveIrm /// @author Morpho Labs /// @custom:contact security@morpho.org -contract AdaptativeCurveIrm is IIrm { +contract AdaptiveCurveIrm is IIrm { using MathLib for int256; using UtilsLib for int256; using MorphoMathLib for uint128; diff --git a/test/MathLibTest.sol b/test/MathLibTest.sol index 6ca864b5..4f11dc2b 100644 --- a/test/MathLibTest.sol +++ b/test/MathLibTest.sol @@ -5,7 +5,7 @@ import {MathLib} from "../src/libraries/MathLib.sol"; import {ErrorsLib} from "../src/libraries/ErrorsLib.sol"; import {wadExp} from "../lib/solmate/src/utils/SignedWadMath.sol"; -import {AdaptativeCurveIrm} from "../src/SpeedJumpIrm.sol"; +import {AdaptiveCurveIrm} from "../src/SpeedJumpIrm.sol"; import "../lib/forge-std/src/Test.sol"; contract MathLibTest is Test { diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index bea89ca9..0f3e1322 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -5,7 +5,7 @@ import "../src/SpeedJumpIrm.sol"; import "../lib/forge-std/src/Test.sol"; -contract AdaptativeCurveIrmTest is Test { +contract AdaptiveCurveIrmTest is Test { using MathLib for int256; using MathLib for int256; using MathLib for uint256; @@ -21,16 +21,16 @@ contract AdaptativeCurveIrmTest is Test { int256 internal constant TARGET_UTILIZATION = 0.9 ether; int256 internal constant INITIAL_RATE_AT_TARGET = int256(0.01 ether) / 365 days; - AdaptativeCurveIrm internal irm; + AdaptiveCurveIrm internal irm; MarketParams internal marketParams = MarketParams(address(0), address(0), address(0), address(0), 0); function setUp() public { irm = - new AdaptativeCurveIrm(address(this), CURVE_STEEPNESS, ADJUSTMENT_SPEED, TARGET_UTILIZATION, INITIAL_RATE_AT_TARGET); + new AdaptiveCurveIrm(address(this), CURVE_STEEPNESS, ADJUSTMENT_SPEED, TARGET_UTILIZATION, INITIAL_RATE_AT_TARGET); vm.warp(90 days); bytes4[] memory selectors = new bytes4[](1); - selectors[0] = AdaptativeCurveIrmTest.handleBorrowRate.selector; + selectors[0] = AdaptiveCurveIrmTest.handleBorrowRate.selector; targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); targetContract(address(this)); } @@ -39,7 +39,7 @@ contract AdaptativeCurveIrmTest is Test { function testDeployment() public { vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - new AdaptativeCurveIrm(address(0), 0, 0, 0, 0); + new AdaptiveCurveIrm(address(0), 0, 0, 0, 0); } function testFirstBorrowRateUtilizationZero() public { @@ -227,12 +227,12 @@ contract AdaptativeCurveIrmTest is Test { totalSupplyAssets = bound(totalSupplyAssets, 0, type(uint128).max); totalBorrowAssets = bound(totalBorrowAssets, 0, totalSupplyAssets); - vm.warp(block.timestamp + elapsed); - Market memory market; + market.lastUpdate = uint128(block.timestamp); market.totalBorrowAssets = uint128(totalSupplyAssets); market.totalSupplyAssets = uint128(totalBorrowAssets); + vm.warp(block.timestamp + elapsed); irm.borrowRate(marketParams, market); } @@ -256,6 +256,8 @@ contract AdaptativeCurveIrmTest is Test { assertLe(irm.borrowRate(marketParams, market), uint256(irm.MAX_RATE_AT_TARGET().wMulDown(CURVE_STEEPNESS))); } + /* HELPERS */ + function _expectedRateAtTarget(Id id, Market memory market) internal view returns (int256) { int256 rateAtTarget = int256(irm.rateAtTarget(id)); int256 speed = ADJUSTMENT_SPEED.wMulDown(_err(market)); From 8fbaffc4c28f434f5a27a44107274cc5d86e4c92 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 15:24:45 +0100 Subject: [PATCH 17/25] docs: minor fix --- src/SpeedJumpIrm.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 429e385f..6bed43cc 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -143,10 +143,10 @@ contract AdaptiveCurveIrm is IIrm { // Riemann (a=0, b=N-1) is done when the rate goes up (err>0) and a right Riemann (a=1, b=N) is done when // the rate goes down (err<0). // avg ~= 1/T Σ_i=a^b curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N - // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearVariation/N * i), err) / N + // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearAdaptation/N * i), err) / N // curve is linear in startRateAtTarget, so: - // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i), err) / N - // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearVariation/N * i) / N, err) + // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearAdaptation/N * i), err) / N + // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearAdaptation/N * i) / N, err) int256 sumRateAtTarget; int256 step = linearAdaptation / N_STEPS; // Compute the terms 1 to N_STEPS - 1. From 13874364d487cd878145a411d0a4f8f351c809f1 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 15:30:38 +0100 Subject: [PATCH 18/25] docs: format doc --- src/SpeedJumpIrm.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index c49bf476..dcc78d21 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -144,10 +144,8 @@ contract AdaptiveCurveIrm is IIrm { // 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 a Riemann sum (steps of length T/N). To underestimate the rate, a - // left - // Riemann (a=0, b=N-1) is done when the rate goes up (err>0) and a right Riemann (a=1, b=N) is done - // when - // the rate goes down (err<0). + // left Riemann (a=0, b=N-1) is done when the rate goes up (err>0) and a right Riemann (a=1, b=N) is + // done when the rate goes down (err<0). // avg ~= 1/T Σ_i=a^b curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearAdaptation/N * i), err) / N // curve is linear in startRateAtTarget, so: From 52b93071a51cb285a4b960f6fff8cc46db1d4672 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 15:48:40 +0100 Subject: [PATCH 19/25] refactor: riemann --- src/SpeedJumpIrm.sol | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index dcc78d21..97053ce2 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -124,10 +124,13 @@ contract AdaptiveCurveIrm is IIrm { int256 startRateAtTarget = rateAtTarget[id]; + int256 avgRateAtTarget; + int256 endRateAtTarget; + if (startRateAtTarget == 0) { // First interaction. - // Safe "unchecked" cast because INITIAL_RATE_AT_TARGET >= 0. - return (uint256(_curve(INITIAL_RATE_AT_TARGET, err)), INITIAL_RATE_AT_TARGET); + avgRateAtTarget = INITIAL_RATE_AT_TARGET; + endRateAtTarget = INITIAL_RATE_AT_TARGET; } else { // 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. @@ -139,7 +142,8 @@ contract AdaptiveCurveIrm is IIrm { if (linearAdaptation == 0) { // If linearAdaptation == 0, avgRateAtTarget = endRateAtTarget = startRateAtTarget; - return (uint256(_curve(startRateAtTarget, err)), 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 @@ -157,15 +161,15 @@ contract AdaptiveCurveIrm is IIrm { for (int256 k = 1; k < N_STEPS; k++) { sumRateAtTarget += _newRateAtTarget(startRateAtTarget, step * k); } - int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); + endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); // Add the term 0 for a left Riemann or the term N_STEPS for a right Riemann. sumRateAtTarget += err < 0 ? endRateAtTarget : startRateAtTarget; - int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; - - // Safe "unchecked" cast because avgRateAtTarget >= 0. - return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); + avgRateAtTarget = sumRateAtTarget / N_STEPS; } } + + // Safe "unchecked" cast because avgRateAtTarget >= 0. + return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); } /// @dev Returns the rate for a given `_rateAtTarget` and an `err`. From 9dac5b917e575d940f16812bb66ea3097d173113 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 16:08:41 +0100 Subject: [PATCH 20/25] feat: trapezoidal riemann (n=2) --- src/SpeedJumpIrm.sol | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index dcc78d21..ed3d2f9b 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -141,26 +141,9 @@ contract AdaptiveCurveIrm is IIrm { // If linearAdaptation == 0, avgRateAtTarget = endRateAtTarget = startRateAtTarget; return (uint256(_curve(startRateAtTarget, err)), 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 a Riemann sum (steps of length T/N). To underestimate the rate, a - // left Riemann (a=0, b=N-1) is done when the rate goes up (err>0) and a right Riemann (a=1, b=N) is - // done when the rate goes down (err<0). - // avg ~= 1/T Σ_i=a^b curve(startRateAtTarget * exp(speed * T/N * i), err) * T / N - // ~= Σ_i=a^b curve(startRateAtTarget * exp(linearAdaptation/N * i), err) / N - // curve is linear in startRateAtTarget, so: - // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearAdaptation/N * i), err) / N - // ~= curve(Σ_i=a^b startRateAtTarget * exp(linearAdaptation/N * i) / N, err) - int256 sumRateAtTarget; - int256 step = linearAdaptation / N_STEPS; - // Compute the terms 1 to N_STEPS - 1. - for (int256 k = 1; k < N_STEPS; k++) { - sumRateAtTarget += _newRateAtTarget(startRateAtTarget, step * k); - } int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); - // Add the term 0 for a left Riemann or the term N_STEPS for a right Riemann. - sumRateAtTarget += err < 0 ? endRateAtTarget : startRateAtTarget; - int256 avgRateAtTarget = sumRateAtTarget / N_STEPS; + int256 midRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation / 2); + int256 avgRateAtTarget = (startRateAtTarget + midRateAtTarget + midRateAtTarget + endRateAtTarget) / 4; // Safe "unchecked" cast because avgRateAtTarget >= 0. return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); From c1633da7c2f36fe504a3301542c5939dbea67c3b Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 18:09:25 +0100 Subject: [PATCH 21/25] docs --- src/SpeedJumpIrm.sol | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index ed3d2f9b..655d872c 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -10,10 +10,6 @@ import {MarketParamsLib} from "../lib/morpho-blue/src/libraries/MarketParamsLib. import {Id, MarketParams, Market} from "../lib/morpho-blue/src/interfaces/IMorpho.sol"; import {MathLib as MorphoMathLib} from "../lib/morpho-blue/src/libraries/MathLib.sol"; -/// @dev Number of steps used in the Riemann sum. -/// @dev 4 steps allows to have a relative error below 30% for 15 days at err=1 or err=-1. -int256 constant N_STEPS = 4; - /// @title AdaptiveCurveIrm /// @author Morpho Labs /// @custom:contact security@morpho.org @@ -141,9 +137,22 @@ contract AdaptiveCurveIrm is IIrm { // If linearAdaptation == 0, avgRateAtTarget = endRateAtTarget = startRateAtTarget; return (uint256(_curve(startRateAtTarget, err)), 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 (f((i-1) * T/N) + f(i * T/N)) / 2 * T/N + // Where f(x) = curve(startRateAtTarget * exp(speed * x), err). + // avg ~= 1/N * ( (f(0)+f(T))/2 + Σ_i=1^(N-1) f(i*T/N) ) + // avg ~= 1/N * ( (f(startRateAtTarget)+f(endRateAtTarget))/2 + Σ_i=1^(N-1) curve(startRateAtTarget * exp(speed * i*T/N), err)) + // As curve is linear in rateAtTarget: + // avg ~= 1/N * curve((startRateAtTarget+endRateAtTarget)/2 + Σ_i=1^(N-1) startRateAtTarget * exp(speed * i*T/N), err) + // avg ~= curve((startRateAtTarget+endRateAtTarget)/(2*N) + Σ_i=1^(N-1) startRateAtTarget * exp(speed * i*T/N) / N, err) + // With N=2: + // avg ~= curve((startRateAtTarget+endRateAtTarget)/4 + startRateAtTarget * exp(speed * T/2) / 2, err) + // avg ~= curve((startRateAtTarget + endRateAtTarget + 2 * startRateAtTarget * exp(speed * T/2)) / 4, err) int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); int256 midRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation / 2); - int256 avgRateAtTarget = (startRateAtTarget + midRateAtTarget + midRateAtTarget + endRateAtTarget) / 4; + int256 avgRateAtTarget = (startRateAtTarget + 2 * midRateAtTarget + endRateAtTarget) / 4; // Safe "unchecked" cast because avgRateAtTarget >= 0. return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget); From f75a09e6892dd15919d2c1fdda8b1aef49f90459 Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 18:09:47 +0100 Subject: [PATCH 22/25] chore: fmt --- src/SpeedJumpIrm.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 655d872c..46d2f992 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -143,13 +143,17 @@ contract AdaptiveCurveIrm is IIrm { // avg ~= 1/T Σ_i=1^N (f((i-1) * T/N) + f(i * T/N)) / 2 * T/N // Where f(x) = curve(startRateAtTarget * exp(speed * x), err). // avg ~= 1/N * ( (f(0)+f(T))/2 + Σ_i=1^(N-1) f(i*T/N) ) - // avg ~= 1/N * ( (f(startRateAtTarget)+f(endRateAtTarget))/2 + Σ_i=1^(N-1) curve(startRateAtTarget * exp(speed * i*T/N), err)) + // avg ~= 1/N * ( (f(startRateAtTarget)+f(endRateAtTarget))/2 + Σ_i=1^(N-1) curve(startRateAtTarget * + // exp(speed * i*T/N), err)) // As curve is linear in rateAtTarget: - // avg ~= 1/N * curve((startRateAtTarget+endRateAtTarget)/2 + Σ_i=1^(N-1) startRateAtTarget * exp(speed * i*T/N), err) - // avg ~= curve((startRateAtTarget+endRateAtTarget)/(2*N) + Σ_i=1^(N-1) startRateAtTarget * exp(speed * i*T/N) / N, err) + // avg ~= 1/N * curve((startRateAtTarget+endRateAtTarget)/2 + Σ_i=1^(N-1) startRateAtTarget * exp(speed + // * i*T/N), err) + // avg ~= curve((startRateAtTarget+endRateAtTarget)/(2*N) + Σ_i=1^(N-1) startRateAtTarget * exp(speed * + // i*T/N) / N, err) // With N=2: // avg ~= curve((startRateAtTarget+endRateAtTarget)/4 + startRateAtTarget * exp(speed * T/2) / 2, err) - // avg ~= curve((startRateAtTarget + endRateAtTarget + 2 * startRateAtTarget * exp(speed * T/2)) / 4, err) + // avg ~= curve((startRateAtTarget + endRateAtTarget + 2 * startRateAtTarget * exp(speed * T/2)) / 4, + // err) int256 endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation); int256 midRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation / 2); int256 avgRateAtTarget = (startRateAtTarget + 2 * midRateAtTarget + endRateAtTarget) / 4; From e46719d87f3913211d16a59d4dfdf8694a01f1d5 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 16 Nov 2023 18:52:04 +0100 Subject: [PATCH 23/25] docs: more concise comments for trapeze N=2 --- src/SpeedJumpIrm.sol | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 072d98cf..db10506c 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -142,25 +142,21 @@ contract AdaptiveCurveIrm is IIrm { 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 + // 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 (f((i-1) * T/N) + f(i * T/N)) / 2 * T/N - // Where f(x) = curve(startRateAtTarget * exp(speed * x), err). - // avg ~= 1/N * ( (f(0)+f(T))/2 + Σ_i=1^(N-1) f(i*T/N) ) - // avg ~= 1/N * ( (f(startRateAtTarget)+f(endRateAtTarget))/2 + Σ_i=1^(N-1) curve(startRateAtTarget * - // exp(speed * i*T/N), err)) - // As curve is linear in rateAtTarget: - // avg ~= 1/N * curve((startRateAtTarget+endRateAtTarget)/2 + Σ_i=1^(N-1) startRateAtTarget * exp(speed - // * i*T/N), err) - // avg ~= curve((startRateAtTarget+endRateAtTarget)/(2*N) + Σ_i=1^(N-1) startRateAtTarget * exp(speed * - // i*T/N) / N, err) - // With N=2: - // avg ~= curve((startRateAtTarget+endRateAtTarget)/4 + startRateAtTarget * exp(speed * T/2) / 2, err) - // avg ~= curve((startRateAtTarget + endRateAtTarget + 2 * startRateAtTarget * exp(speed * T/2)) / 4, - // err) + // 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 + 2 * midRateAtTarget + endRateAtTarget) / 4; + avgRateAtTarget = (startRateAtTarget + endRateAtTarget + 2 * midRateAtTarget) / 4; } } From 76679917a4b1e4da903814eadf407228b9d609cc Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Thu, 16 Nov 2023 18:54:14 +0100 Subject: [PATCH 24/25] fix: missing bracket --- src/SpeedJumpIrm.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index db10506c..3ea03046 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -148,7 +148,7 @@ contract AdaptiveCurveIrm is IIrm { // 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([Σ_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: From f0ebd8b71d37af90d0523be4c1d8d3835a2d4aef Mon Sep 17 00:00:00 2001 From: MathisGD Date: Thu, 16 Nov 2023 19:13:54 +0100 Subject: [PATCH 25/25] test: larger approx --- test/SpeedJumpIrmTest.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index 0f3e1322..fdf3b864 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -172,7 +172,7 @@ contract AdaptiveCurveIrmTest is Test { uint256 borrowRate = irm.borrowRate(marketParams, market1); assertEq(borrowRateView, borrowRate, "borrowRateView"); - assertApproxEqRel(borrowRate, expectedAvgRate, 0.1 ether, "avgBorrowRate"); + assertApproxEqRel(borrowRate, expectedAvgRate, 0.11 ether, "avgBorrowRate"); assertApproxEqRel(irm.rateAtTarget(marketParams.id()), expectedRateAtTarget, 0.001 ether, "rateAtTarget"); }