Skip to content

Commit

Permalink
Merge pull request #82 from morpho-org/feat/riemann-avg
Browse files Browse the repository at this point in the history
Riemann average
  • Loading branch information
MerlinEgalite authored Nov 16, 2023
2 parents d68db06 + f0ebd8b commit 4637e31
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 61 deletions.
81 changes: 46 additions & 35 deletions src/SpeedJumpIrm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +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";

/// @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;
Expand Down Expand Up @@ -88,8 +88,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
Expand All @@ -98,17 +98,17 @@ 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));
// Safe "unchecked" cast because endRateAtTarget >= 0.
emit BorrowRateUpdate(id, avgRate, uint256(endRateAtTarget));

return avgBorrowRate;
return avgRate;
}

/// @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).
Expand All @@ -120,44 +120,48 @@ contract AdaptativeCurveIrm is IIrm {

int256 startRateAtTarget = rateAtTarget[id];

// First interaction.
int256 avgRateAtTarget;
int256 endRateAtTarget;

if (startRateAtTarget == 0) {
return (uint256(_curve(INITIAL_RATE_AT_TARGET, err)), INITIAL_RATE_AT_TARGET);
// First interaction.
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.
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;
// If linearAdaptation == 0, avgRateAtTarget = endRateAtTarget = startRateAtTarget;
avgRateAtTarget = startRateAtTarget;
endRateAtTarget = startRateAtTarget;
} else {
int256 startBorrowRate = _curve(startRateAtTarget, err);
avgBorrowRate = (endBorrowRate - startBorrowRate).wDivDown(linearAdaptation);
// 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;
}

// avgBorrowRate is non negative because:
// - endBorrowRate >= 0 because endRateAtTarget >= MIN_RATE_AT_TARGET.
// - linearAdaptation < 0 <=> adaptationMultiplier <= 1 <=> endBorrowRate <= startBorrowRate.
return (uint256(avgBorrowRate), endRateAtTarget);
}

// Safe "unchecked" cast because avgRateAtTarget >= 0.
return (uint256(_curve(avgRateAtTarget, err)), endRateAtTarget);
}

/// @dev Returns the rate for a given `_rateAtTarget` and an `err`.
Expand All @@ -167,7 +171,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: 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);
}
}
2 changes: 1 addition & 1 deletion test/MathLibTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
52 changes: 27 additions & 25 deletions test/SpeedJumpIrmTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
}
Expand All @@ -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 {
Expand Down 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);

// (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.11 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 All @@ -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);
}

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

0 comments on commit 4637e31

Please sign in to comment.