diff --git a/src/SpeedJumpIrm.sol b/src/SpeedJumpIrm.sol index 96084039..1c3aa13d 100644 --- a/src/SpeedJumpIrm.sol +++ b/src/SpeedJumpIrm.sol @@ -10,12 +10,7 @@ import {MarketParamsLib} from "morpho-blue/libraries/MarketParamsLib.sol"; import {Id, MarketParams, Market} from "morpho-blue/interfaces/IMorpho.sol"; import {WAD, MathLib as MorphoMathLib} from "morpho-blue/libraries/MathLib.sol"; -struct MarketIrm { - // Previous final borrow rate. Scaled by WAD. - uint128 prevBorrowRate; - // Previous error. Scaled by WAD. - int128 prevErr; -} +import "forge-std/console.sol"; /// @title SpeedJumpIrm /// @author Morpho Labs @@ -32,7 +27,7 @@ contract SpeedJumpIrm is IIrm { /* EVENTS */ /// @notice Emitted when a borrow rate is updated. - event BorrowRateUpdate(Id indexed id, int128 err, uint128 newBorrowRate, uint256 avgBorrowRate); + event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 baseRate); /* CONSTANTS */ @@ -51,12 +46,12 @@ contract SpeedJumpIrm is IIrm { /// @notice Target utilization (scaled by WAD). uint256 public immutable TARGET_UTILIZATION; /// @notice Initial rate (scaled by WAD). - uint128 public immutable INITIAL_RATE; + uint256 public immutable INITIAL_BASE_RATE; /* STORAGE */ - /// @notice IRM storage for each market. - mapping(Id => MarketIrm) public marketIrm; + /// @notice Base rate of markets. + mapping(Id => uint256) public baseRate; /* CONSTRUCTOR */ @@ -65,13 +60,13 @@ contract SpeedJumpIrm is IIrm { /// @param lnJumpFactor The log of the jump factor (scaled by WAD). /// @param speedFactor The speed factor (scaled by WAD). /// @param targetUtilization The target utilization (scaled by WAD). Should be strictly between 0 and 1. - /// @param initialRate The initial rate (scaled by WAD). + /// @param initialBaseRate The initial rate (scaled by WAD). constructor( address morpho, uint256 lnJumpFactor, uint256 speedFactor, uint256 targetUtilization, - uint128 initialRate + uint256 initialBaseRate ) { require(lnJumpFactor <= uint256(type(int256).max), ErrorsLib.INPUT_TOO_LARGE); require(speedFactor <= uint256(type(int256).max), ErrorsLib.INPUT_TOO_LARGE); @@ -82,14 +77,14 @@ contract SpeedJumpIrm is IIrm { LN_JUMP_FACTOR = lnJumpFactor; SPEED_FACTOR = speedFactor; TARGET_UTILIZATION = targetUtilization; - INITIAL_RATE = initialRate; + INITIAL_BASE_RATE = initialBaseRate; } /* BORROW RATES */ /// @inheritdoc IIrm function borrowRateView(MarketParams memory marketParams, Market memory market) external view returns (uint256) { - (,, uint256 avgBorrowRate) = _borrowRate(marketParams.id(), market); + (uint256 avgBorrowRate,) = _borrowRate(marketParams.id(), market); return avgBorrowRate; } @@ -99,44 +94,32 @@ contract SpeedJumpIrm is IIrm { Id id = marketParams.id(); - (int128 err, uint128 newBorrowRate, uint256 avgBorrowRate) = _borrowRate(id, market); + (uint256 avgBorrowRate, uint256 newBaseRate) = _borrowRate(id, market); - marketIrm[id].prevErr = err; - marketIrm[id].prevBorrowRate = newBorrowRate; + baseRate[id] = newBaseRate; - emit BorrowRateUpdate(id, err, newBorrowRate, avgBorrowRate); + emit BorrowRateUpdate(id, avgBorrowRate, newBaseRate); return avgBorrowRate; } /// @dev Returns err, newBorrowRate and avgBorrowRate. - function _borrowRate(Id id, Market memory market) private view returns (int128, uint128, uint128) { + function _borrowRate(Id id, Market memory market) private view returns (uint256, uint256) { uint256 utilization = market.totalSupplyAssets > 0 ? market.totalBorrowAssets.wDivDown(market.totalSupplyAssets) : 0; uint256 errNormFactor = utilization > TARGET_UTILIZATION ? WAD - TARGET_UTILIZATION : TARGET_UTILIZATION; - // Safe "unchecked" int128 cast because |err| <= WAD. // Safe "unchecked" int256 casts because utilization <= WAD, TARGET_UTILIZATION < WAD and errNormFactor <= WAD. - int128 err = int128((int256(utilization) - int256(TARGET_UTILIZATION)).wDivDown(int256(errNormFactor))); - - if (marketIrm[id].prevBorrowRate == 0) return (err, INITIAL_RATE, INITIAL_RATE); + int256 err = (int256(utilization) - int256(TARGET_UTILIZATION)).wDivDown(int256(errNormFactor)); - // errDelta = err - prevErr. - // errDelta is between -1 and 1, scaled by WAD. - int256 errDelta = err - marketIrm[id].prevErr; - - // Safe "unchecked" cast because LN_JUMP_FACTOR <= type(int256).max. - uint256 jumpMultiplier = MathLib.wExp(errDelta.wMulDown(int256(LN_JUMP_FACTOR))); // Safe "unchecked" cast because SPEED_FACTOR <= type(int256).max. int256 speed = int256(SPEED_FACTOR).wMulDown(err); - uint256 elapsed = block.timestamp - market.lastUpdate; + uint256 elapsed = (baseRate[id] > 0) ? block.timestamp - market.lastUpdate : 0; // Safe "unchecked" cast because elapsed <= block.timestamp. int256 linearVariation = speed * int256(elapsed); uint256 variationMultiplier = MathLib.wExp(linearVariation); - - // newBorrowRate = prevBorrowRate * jumpMultiplier * variationMultiplier. - uint256 borrowRateAfterJump = marketIrm[id].prevBorrowRate.wMulDown(jumpMultiplier); - uint256 newBorrowRate = borrowRateAfterJump.wMulDown(variationMultiplier); + uint256 newBaseRate = (baseRate[id] > 0) ? baseRate[id].wMulDown(variationMultiplier) : INITIAL_BASE_RATE; + uint256 newBorrowRate = newBaseRate.wMulDown(MathLib.wExp(err)); // Then we compute the average rate over the period (this is what Morpho needs to accrue the interest). // avgBorrowRate = 1 / elapsed * ∫ borrowRateAfterJump * exp(speed * t) dt between 0 and elapsed @@ -144,11 +127,16 @@ contract SpeedJumpIrm is IIrm { // = (newBorrowRate - borrowRateAfterJump) / linearVariation // And avgBorrowRate ~ borrowRateAfterJump for linearVariation around zero. uint256 avgBorrowRate; - if (linearVariation == 0) avgBorrowRate = borrowRateAfterJump; - // Safe "unchecked" cast to uint256 because linearVariation < 0 <=> newBorrowRate <= borrowRateAfterJump. - else avgBorrowRate = uint256((int256(newBorrowRate) - int256(borrowRateAfterJump)).wDivDown(linearVariation)); + if (linearVariation == 0 || baseRate[id] == 0) { + avgBorrowRate = newBorrowRate; + } else { + // Safe "unchecked" cast to uint256 because linearVariation < 0 <=> newBorrowRate <= borrowRateAfterJump. + avgBorrowRate = uint256( + (int256(newBorrowRate) - int256(baseRate[id].wMulDown(MathLib.wExp(err)))).wDivDown(linearVariation) + ); + } // We bound both newBorrowRate and avgBorrowRate between MIN_RATE and MAX_RATE. - return (err, uint128(newBorrowRate.bound(MIN_RATE, MAX_RATE)), uint128(avgBorrowRate.bound(MIN_RATE, MAX_RATE))); + return (avgBorrowRate, newBaseRate); } } diff --git a/test/SpeedJumpIrmTest.sol b/test/SpeedJumpIrmTest.sol index 77e0a6c4..5c794ae2 100644 --- a/test/SpeedJumpIrmTest.sol +++ b/test/SpeedJumpIrmTest.sol @@ -13,31 +13,41 @@ contract SpeedJumpIrmTest is Test { using MorphoMathLib for uint256; using MarketParamsLib for MarketParams; - event BorrowRateUpdate(Id indexed id, int128 err, uint128 newBorrowRate, uint256 avgBorrowRate); + event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 baseBorrowRate); uint256 internal constant LN2 = 0.69314718056 ether; uint256 internal constant TARGET_UTILIZATION = 0.8 ether; uint256 internal constant SPEED_FACTOR = uint256(0.01 ether) / uint256(10 hours); - uint128 internal constant INITIAL_RATE = uint128(0.01 ether) / uint128(365 days); + // rate for utilization=0 and baseRate=initialBaseRate. + uint256 internal constant INITIAL_RATE = uint128(0.01 ether) / uint128(365 days); + uint256 internal constant INITIAL_BASE_RATE = uint256(INITIAL_RATE) * LN2 / 1 ether; SpeedJumpIrm internal irm; MarketParams internal marketParams = MarketParams(address(0), address(0), address(0), address(0), 0); function setUp() public { - irm = new SpeedJumpIrm(address(this), LN2, SPEED_FACTOR, TARGET_UTILIZATION, INITIAL_RATE); + irm = new SpeedJumpIrm(address(this), LN2, SPEED_FACTOR, TARGET_UTILIZATION, INITIAL_BASE_RATE); vm.warp(90 days); } + function testFirstBorrowRateEmptyMarket() public { + Market memory market; + uint256 avgBorrowRate = irm.borrowRate(marketParams, market); + uint256 baseRate = irm.baseRate(marketParams.id()); + + assertEq(avgBorrowRate, INITIAL_BASE_RATE.wMulDown(MathLib.wExp(-1 ether)), "avgBorrowRate"); + assertEq(baseRate, INITIAL_BASE_RATE, "baseRate"); + } + function testFirstBorrowRate(Market memory market) public { vm.assume(market.totalBorrowAssets > 0); vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); uint256 avgBorrowRate = irm.borrowRate(marketParams, market); - (uint256 prevBorrowRate, int256 prevErr) = irm.marketIrm(marketParams.id()); + uint256 baseRate = irm.baseRate(marketParams.id()); - assertEq(avgBorrowRate, INITIAL_RATE, "avgBorrowRate"); - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(prevErr, _err(market), "prevErr"); + assertEq(avgBorrowRate, INITIAL_BASE_RATE.wMulDown(MathLib.wExp(_err(market))), "avgBorrowRate"); + assertEq(baseRate, INITIAL_BASE_RATE, "baseRate"); } function testBorrowRateEventEmission(Market memory market) public { @@ -45,7 +55,7 @@ contract SpeedJumpIrmTest is Test { vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); vm.expectEmit(address(irm)); - emit BorrowRateUpdate(marketParams.id(), int128(_err(market)), INITIAL_RATE, INITIAL_RATE); + emit BorrowRateUpdate(marketParams.id(), INITIAL_RATE, INITIAL_RATE); irm.borrowRate(marketParams, market); } @@ -54,11 +64,10 @@ contract SpeedJumpIrmTest is Test { vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); uint256 avgBorrowRate = irm.borrowRateView(marketParams, market); - (uint256 prevBorrowRate, int256 prevErr) = irm.marketIrm(marketParams.id()); + uint256 baseRate = irm.baseRate(marketParams.id()); - assertEq(avgBorrowRate, INITIAL_RATE, "avgBorrowRate"); - assertEq(prevBorrowRate, 0, "prevBorrowRate"); - assertEq(prevErr, 0, "prevErr"); + assertEq(avgBorrowRate, INITIAL_BASE_RATE.wMulDown(MathLib.wExp(_err(market))), "avgBorrowRate"); + assertEq(baseRate, 0, "prevBorrowRate"); } function testBorrowRate(Market memory market0, Market memory market1) public { @@ -70,13 +79,12 @@ contract SpeedJumpIrmTest is Test { vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); - uint256 avgBorrowRate = irm.borrowRate(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); - - (uint256 expectedAvgBorrowRate, uint256 expectedPrevBorrowRate) = _expectedBorrowRates(market0, market1); + uint256 expectedBaseRate = _expectedBaseRate(marketParams.id(), market1); - assertEq(prevBorrowRate, expectedPrevBorrowRate, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + assertApproxEqRel( + irm.borrowRate(marketParams, market1), _expectedAvgRate(market0, market1), 0.01 ether, "avgBorrowRate" + ); + assertApproxEqRel(irm.baseRate(marketParams.id()), expectedBaseRate, 0.001 ether, "baseRate"); } function testBorrowRateView(Market memory market0, Market memory market1) public { @@ -88,13 +96,9 @@ contract SpeedJumpIrmTest is Test { vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); - uint256 avgBorrowRate = irm.borrowRateView(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); - - (uint256 expectedAvgBorrowRate,) = _expectedBorrowRates(market0, market1); - - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + assertApproxEqRel( + irm.borrowRateView(marketParams, market1), _expectedAvgRate(market0, market1), 0.01 ether, "avgBorrowRate" + ); } function testBorrowRateJumpOnly(Market memory market0, Market memory market1) public { @@ -106,14 +110,12 @@ contract SpeedJumpIrmTest is Test { vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); market1.lastUpdate = uint128(block.timestamp); - uint256 avgBorrowRate = irm.borrowRate(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); - - (uint256 expectedAvgBorrowRate, uint256 expectedPrevBorrowRate) = _expectedBorrowRates(market0, market1); - - assertEq(expectedAvgBorrowRate, expectedPrevBorrowRate, "expectedAvgBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); - assertEq(prevBorrowRate, expectedPrevBorrowRate, "prevBorrowRate"); + assertApproxEqRel( + irm.borrowRate(marketParams, market1), _expectedAvgRate(market0, market1), 0.01 ether, "avgBorrowRate" + ); + assertApproxEqRel( + irm.baseRate(marketParams.id()), _expectedBaseRate(marketParams.id(), market1), 0.001 ether, "baseRate" + ); } function testBorrowRateViewJumpOnly(Market memory market0, Market memory market1) public { @@ -125,13 +127,9 @@ contract SpeedJumpIrmTest is Test { vm.assume(market1.totalSupplyAssets >= market1.totalBorrowAssets); market1.lastUpdate = uint128(block.timestamp); - uint256 avgBorrowRate = irm.borrowRateView(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); - - (uint256 expectedAvgBorrowRate,) = _expectedBorrowRates(market0, market1); - - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + assertApproxEqRel( + irm.borrowRateView(marketParams, market1), _expectedAvgRate(market0, market1), 0.01 ether, "avgBorrowRate" + ); } function testBorrowRateSpeedOnly(Market memory market0, Market memory market1) public { @@ -143,13 +141,12 @@ contract SpeedJumpIrmTest is Test { market1.totalSupplyAssets = market0.totalSupplyAssets; market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); - uint256 avgBorrowRate = irm.borrowRate(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); - - (uint256 expectedAvgBorrowRate, uint256 expectedPrevBorrowRate) = _expectedBorrowRates(market0, market1); + uint256 expectedBaseRate = _expectedBaseRate(marketParams.id(), market1); - assertEq(prevBorrowRate, expectedPrevBorrowRate, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + assertApproxEqRel( + irm.borrowRate(marketParams, market1), _expectedAvgRate(market0, market1), 0.01 ether, "avgBorrowRate" + ); + assertApproxEqRel(irm.baseRate(marketParams.id()), expectedBaseRate, 0.001 ether, "baseRate"); } function testBorrowRateViewSpeedOnly(Market memory market0, Market memory market1) public { @@ -161,31 +158,35 @@ contract SpeedJumpIrmTest is Test { market1.totalSupplyAssets = market0.totalSupplyAssets; market1.lastUpdate = uint128(bound(market1.lastUpdate, 0, block.timestamp - 1)); - uint256 avgBorrowRate = irm.borrowRateView(marketParams, market1); - (uint256 prevBorrowRate,) = irm.marketIrm(marketParams.id()); + assertApproxEqRel( + irm.borrowRateView(marketParams, market1), _expectedAvgRate(market0, market1), 0.01 ether, "avgBorrowRate" + ); + } - (uint256 expectedAvgBorrowRate,) = _expectedBorrowRates(market0, market1); + function _expectedBaseRate(Id id, Market memory market) internal view returns (uint256) { + uint256 baseRate = irm.baseRate(id); + console.log(irm.baseRate(id)); - assertEq(prevBorrowRate, INITIAL_RATE, "prevBorrowRate"); - assertEq(avgBorrowRate, expectedAvgBorrowRate, "avgBorrowRate"); + int256 speed = int256(SPEED_FACTOR).wMulDown(_err(market)); + uint256 elapsed = (baseRate > 0) ? block.timestamp - market.lastUpdate : 0; + int256 linearVariation = speed * int256(elapsed); + uint256 variationMultiplier = MathLib.wExp(linearVariation); + return (baseRate > 0) ? baseRate.wMulDown(variationMultiplier) : INITIAL_BASE_RATE; } - /// @dev Returns the expected `avgBorrowRate` and `prevBorrowRate`. - function _expectedBorrowRates(Market memory market0, Market memory market1) - internal - view - returns (uint256, uint256) - { - int256 err = _err(market1); + /// @dev Returns the expected `avgBorrowRate` and `baseBorrowRate`. + function _expectedAvgRate(Market memory market0, Market memory market1) internal view returns (uint256) { int256 prevErr = _err(market0); + int256 err = _err(market1); int256 errDelta = err - prevErr; - uint256 elapsed = block.timestamp - market1.lastUpdate; + uint256 elapsed = block.timestamp - market0.lastUpdate; uint256 jumpMultiplier = MathLib.wExp(errDelta.wMulDown(int256(LN2))); - int256 speed = int256(SPEED_FACTOR).wMulDown(err); + int256 speed = int256(SPEED_FACTOR).wMulDown(prevErr); uint256 variationMultiplier = MathLib.wExp(speed * int256(elapsed)); - uint256 expectedBorrowRateAfterJump = INITIAL_RATE.wMulDown(jumpMultiplier); - uint256 expectedNewBorrowRate = INITIAL_RATE.wMulDown(jumpMultiplier).wMulDown(variationMultiplier); + uint256 initialRate = INITIAL_BASE_RATE.wMulDown(MathLib.wExp(prevErr)); + uint256 expectedBorrowRateAfterJump = initialRate.wMulDown(jumpMultiplier); + uint256 expectedNewBorrowRate = expectedBorrowRateAfterJump.wMulDown(variationMultiplier); uint256 expectedAvgBorrowRate; if (speed * int256(elapsed) == 0) { @@ -196,13 +197,11 @@ contract SpeedJumpIrmTest is Test { ); } - return ( - expectedAvgBorrowRate.bound(irm.MIN_RATE(), irm.MAX_RATE()), - expectedNewBorrowRate.bound(irm.MIN_RATE(), irm.MAX_RATE()) - ); + return expectedAvgBorrowRate; } function _err(Market memory market) internal pure returns (int256) { + if (market.totalSupplyAssets == 0) return -1 ether; uint256 utilization = market.totalBorrowAssets.wDivDown(market.totalSupplyAssets); int256 err;