diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index f831a1f5..3ea03046 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -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; @@ -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 @@ -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). @@ -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`. @@ -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); + } } 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 57d84e24..fdf3b864 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 { @@ -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.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 { @@ -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.11 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"); } @@ -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));