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

Riemann average #82

Merged
merged 32 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1c3bedf
feat: riemann avg
MathisGD Nov 15, 2023
6e49f0e
style: minor improvements
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
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
41 changes: 18 additions & 23 deletions src/SpeedJumpIrm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ 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;
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

/// @title AdaptativeCurveIrm
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
/// @author Morpho Labs
/// @custom:contact security@morpho.org
Expand Down Expand Up @@ -120,43 +124,34 @@ 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
// 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;
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.
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
// 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.
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
int256 sum;
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
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);
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
MathisGD marked this conversation as resolved.
Show resolved Hide resolved
}
int256 averageRateAtTarget = sum / N_STEPS;
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down
36 changes: 18 additions & 18 deletions test/SpeedJumpIrmTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
MathisGD marked this conversation as resolved.
Show resolved Hide resolved

// (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 {
Expand All @@ -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.724 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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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");
}

Expand Down Expand Up @@ -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);
Expand All @@ -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");
}

Expand Down