Skip to content

Commit

Permalink
feat: refactor curve abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
MathisGD committed Oct 27, 2023
1 parent 3de1b35 commit 2675073
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 104 deletions.
64 changes: 26 additions & 38 deletions src/SpeedJumpIrm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 */

Expand All @@ -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 */

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

Expand All @@ -99,56 +94,49 @@ 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
// = borrowRateAfterJump * (exp(linearVariation) - 1) / linearVariation
// = (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);
}
}
131 changes: 65 additions & 66 deletions test/SpeedJumpIrmTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,49 @@ 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 {
vm.assume(market.totalBorrowAssets > 0);
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);
}

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

0 comments on commit 2675073

Please sign in to comment.