Skip to content

Commit

Permalink
Merge branch 'refactor/curve' of github.com:morpho-labs/morpho-blue-p…
Browse files Browse the repository at this point in the history
…eriphery into fix/exp-overflow-35
  • Loading branch information
Rubilmax committed Nov 10, 2023
2 parents 939d3d0 + 222b04e commit 7f09d96
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 67 deletions.
71 changes: 36 additions & 35 deletions src/SpeedJumpIrm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,16 @@ contract AdaptativeCurveIrm is IIrm {
address public immutable MORPHO;
/// @notice Curve steepness (scaled by WAD).
/// @dev Verified to be greater than 1 at construction.
uint256 public immutable CURVE_STEEPNESS;
int256 public immutable CURVE_STEEPNESS;
/// @notice Adjustment speed (scaled by WAD).
/// @dev The speed is per second, so the rate moves at a speed of ADJUSTMENT_SPEED * err each second (while being
/// continuously compounded). A typical value for the ADJUSTMENT_SPEED would be 10 ether / 365 days.
uint256 public immutable ADJUSTMENT_SPEED;
/// continuously compounded). A typical value for the ADJUSTMENT_SPEED would be 10 ethers / 365 days.
/// @dev Verified to be non-negative at construction.
int256 public immutable ADJUSTMENT_SPEED;
/// @notice Target utilization (scaled by WAD).
/// @dev Verified to be strictly between 0 and 1 at construction.
uint256 public immutable TARGET_UTILIZATION;
/// @notice Initial rate at target (scaled by WAD).
int256 public immutable TARGET_UTILIZATION;
/// @notice Initial rate at target per second (scaled by WAD).
uint256 public immutable INITIAL_RATE_AT_TARGET;

/* STORAGE */
Expand Down Expand Up @@ -78,9 +79,12 @@ contract AdaptativeCurveIrm is IIrm {
require(initialRateAtTarget <= MAX_RATE_AT_TARGET, ErrorsLib.INPUT_TOO_LARGE);

MORPHO = morpho;
CURVE_STEEPNESS = curveSteepness;
ADJUSTMENT_SPEED = adjustmentSpeed;
TARGET_UTILIZATION = targetUtilization;
// Safe "unchecked" cast.
CURVE_STEEPNESS = int256(curveSteepness);
// Safe "unchecked" cast.
ADJUSTMENT_SPEED = int256(adjustmentSpeed);
// Safe "unchecked" cast.
TARGET_UTILIZATION = int256(targetUtilization);
INITIAL_RATE_AT_TARGET = initialRateAtTarget;
}

Expand All @@ -98,63 +102,60 @@ contract AdaptativeCurveIrm is IIrm {

Id id = marketParams.id();

(uint256 avgBorrowRate, uint256 newRateAtTarget) = _borrowRate(id, market);
(uint256 avgBorrowRate, uint256 endRateAtTarget) = _borrowRate(id, market);

rateAtTarget[id] = newRateAtTarget;
rateAtTarget[id] = endRateAtTarget;

emit BorrowRateUpdate(id, avgBorrowRate, newRateAtTarget);
emit BorrowRateUpdate(id, avgBorrowRate, endRateAtTarget);

return avgBorrowRate;
}

/// @dev Returns avgBorrowRate and newRateAtTarget.
/// @dev Returns avgBorrowRate and endRateAtTarget.
/// @dev Assumes that the inputs `marketParams` and `id` match.
function _borrowRate(Id id, Market memory market) private view returns (uint256, uint256) {
uint256 utilization =
market.totalSupplyAssets > 0 ? market.totalBorrowAssets.wDivDown(market.totalSupplyAssets) : 0;
// Safe "unchecked" cast because the utilization is smaller than 1 (scaled by WAD).
int256 utilization =
int256(market.totalSupplyAssets > 0 ? market.totalBorrowAssets.wDivDown(market.totalSupplyAssets) : 0);

uint256 errNormFactor = utilization > TARGET_UTILIZATION ? WAD - TARGET_UTILIZATION : TARGET_UTILIZATION;
// Safe "unchecked" int256 casts because utilization <= WAD, TARGET_UTILIZATION < WAD and errNormFactor <= WAD.
int256 err = (int256(utilization) - int256(TARGET_UTILIZATION)).wDivDown(int256(errNormFactor));
int256 errNormFactor = utilization > TARGET_UTILIZATION ? WAD_INT - TARGET_UTILIZATION : TARGET_UTILIZATION;
int256 err = (utilization - TARGET_UTILIZATION).wDivDown(errNormFactor);

uint256 startRateAtTarget = rateAtTarget[id];

// First interaction.
if (startRateAtTarget == 0) {
return (_curve(INITIAL_RATE_AT_TARGET, err), INITIAL_RATE_AT_TARGET);
} else {
// Safe "unchecked" cast because ADJUSTMENT_SPEED <= type(int256).max.
// 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.
uint256 elapsed = block.timestamp - market.lastUpdate;
// Safe "unchecked" cast because elapsed <= block.timestamp.
int256 linearVariation = speed * int256(elapsed);
/// variationMultiplier may be capped to WEXP_UPPER_VALUE to prevent overflows, which would under-estimate
/// the end rate. In this case, any user can accrue interest on Morpho to update the rate.
uint256 variationMultiplier = MathLib.wExp(linearVariation);
int256 linearAdaptation = speed * int256(elapsed);
uint256 adaptationMultiplier = MathLib.wExp(linearAdaptation);
// endRateAtTarget is bounded between MIN_RATE_AT_TARGET and MAX_RATE_AT_TARGET.
uint256 endRateAtTarget =
startRateAtTarget.wMulDown(variationMultiplier).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET);
startRateAtTarget.wMulDown(adaptationMultiplier).bound(MIN_RATE_AT_TARGET, MAX_RATE_AT_TARGET);
uint256 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(linearVariation) - 1) / linearVariation
// = (endBorrowRate - startBorrowRate) / linearVariation
// And avgBorrowRate ~ startBorrowRate = endBorrowRate for linearVariation around zero.
// = 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).
uint256 avgBorrowRate;
if (linearVariation == 0) {
if (linearAdaptation == 0) {
avgBorrowRate = endBorrowRate;
} else {
uint256 startBorrowRate = _curve(startRateAtTarget, err);
// Safe "unchecked" cast to uint256 because linearVariation < 0 <=> variationMultiplier <= 1.
// Safe "unchecked" cast to uint256 because linearAdaptation < 0 <=> adaptationMultiplier <= 1.
// <=> endBorrowRate <= startBorrowRate.
avgBorrowRate = uint256((int256(endBorrowRate) - int256(startBorrowRate)).wDivDown(linearVariation));
avgBorrowRate = uint256((int256(endBorrowRate) - int256(startBorrowRate)).wDivDown(linearAdaptation));
}

return (avgBorrowRate, endRateAtTarget);
Expand All @@ -166,11 +167,11 @@ contract AdaptativeCurveIrm is IIrm {
/// r = ((1-1/C)*err + 1) * rateAtTarget if err < 0
/// ((C-1)*err + 1) * rateAtTarget else.
function _curve(uint256 _rateAtTarget, int256 err) private view returns (uint256) {
// Safe "unchecked" cast because err >= -1 (in WAD).
if (err < 0) {
return uint256((WAD - WAD.wDivDown(CURVE_STEEPNESS)).wMulDown(err) + WAD_INT).wMulDown(_rateAtTarget);
} else {
return uint256((CURVE_STEEPNESS - WAD).wMulDown(err) + WAD_INT).wMulDown(_rateAtTarget);
}
// Safe "unchecked" cast of _rateAtTarget because _rateAtTarget <= MAX_RATE_AT_TARGET.
int256 steeringCoeff = (err < 0 ? WAD_INT - WAD_INT.wDivDown(CURVE_STEEPNESS) : CURVE_STEEPNESS - WAD_INT)
.wMulDown(int256(_rateAtTarget));
// Safe "unchecked" cast of _rateAtTarget because _rateAtTarget <= MAX_RATE_AT_TARGET.
// Safe "unchecked" cast of the result because r >= 0.
return uint256(steeringCoeff.wMulDown(err) + int256(_rateAtTarget));
}
}
11 changes: 5 additions & 6 deletions src/libraries/MathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ library MathLib {
/// @dev Returns an approximation of exp.
function wExp(int256 x) internal pure returns (uint256) {
unchecked {
// x < ln(1e-18) => exp(x) < 1e-18 so it is rounded to zero.
// If x < ln(1e-18) then exp(x) < 1e-18 so it is rounded to zero.
if (x < LN_WEI_INT) return 0;
if (x >= WEXP_UPPER_BOUND) return WEXP_UPPER_VALUE;

Expand All @@ -38,11 +38,11 @@ library MathLib {
int256 roundingAdjustment = (x < 0) ? -(LN_2_INT / 2) : (LN_2_INT / 2);
// Safe unchecked because x is bounded.
int256 q = (x + roundingAdjustment) / LN_2_INT;
// Safe unchecked because |q * LN_2_INT| <= |x|.
// Safe unchecked because |q * ln(2) - x| <= ln(2)/2.
int256 r = x - q * LN_2_INT;

// Compute e^r with a 2nd-order Taylor polynomial.
// Safe unchecked because |r| < 1, expR < 2 and the sum is positive.
// Safe unchecked because |r| < 1e18, and the sum is positive.
uint256 expR = uint256(WAD_INT + r + (r * r) / WAD_INT / 2);

// Return e^x = 2^q * e^r.
Expand All @@ -51,9 +51,8 @@ library MathLib {
}
}

function wMulDown(uint256 a, int256 b) internal pure returns (int256) {
require(a <= uint256(type(int256).max));
return int256(a) * b / WAD_INT;
function wMulDown(int256 a, int256 b) internal pure returns (int256) {
return a * b / WAD_INT;
}

function wDivDown(int256 a, int256 b) internal pure returns (int256) {
Expand Down
59 changes: 33 additions & 26 deletions test/SpeedJumpIrmTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ contract AdaptativeCurveIrmTest is Test {

event BorrowRateUpdate(Id indexed id, uint256 avgBorrowRate, uint256 rateAtTarget);

uint256 internal constant CURVE_STEEPNESS = 4 ether;
uint256 internal constant ADJUSTMENT_SPEED = 50 ether / uint256(365 days);
uint256 internal constant TARGET_UTILIZATION = 0.9 ether;
int256 internal constant CURVE_STEEPNESS = 4 ether;
int256 internal constant ADJUSTMENT_SPEED = 50 ether / int256(365 days);
int256 internal constant TARGET_UTILIZATION = 0.9 ether;
uint256 internal constant INITIAL_RATE_AT_TARGET = 0.01 ether / uint256(365 days);

AdaptativeCurveIrm 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 AdaptativeCurveIrm(address(this), uint256(CURVE_STEEPNESS), uint256(ADJUSTMENT_SPEED), uint256(TARGET_UTILIZATION), INITIAL_RATE_AT_TARGET);
vm.warp(90 days);
}

Expand All @@ -38,7 +38,9 @@ contract AdaptativeCurveIrmTest is Test {
function testFirstBorrowRateUtilizationZero() public {
Market memory market;

assertEq(irm.borrowRate(marketParams, market), INITIAL_RATE_AT_TARGET / 4, "avgBorrowRate");
assertApproxEqRel(
irm.borrowRate(marketParams, market), INITIAL_RATE_AT_TARGET / 4, 0.0001 ether, "avgBorrowRate"
);
assertEq(irm.rateAtTarget(marketParams.id()), INITIAL_RATE_AT_TARGET, "rateAtTarget");
}

Expand Down Expand Up @@ -178,24 +180,28 @@ contract AdaptativeCurveIrmTest is Test {
Market memory market;
market.totalBorrowAssets = 9 ether;
market.totalSupplyAssets = 10 ether;
assertGt(irm.borrowRate(marketParams, market), irm.MIN_RATE_AT_TARGET().wDivDown(CURVE_STEEPNESS));
assertGt(
irm.borrowRate(marketParams, market), uint256(int256(irm.MIN_RATE_AT_TARGET()).wDivDown(CURVE_STEEPNESS))
);
}

function invariantMaxRateAtTarget() public {
Market memory market;
market.totalBorrowAssets = 9 ether;
market.totalSupplyAssets = 10 ether;
assertLt(irm.borrowRate(marketParams, market), irm.MAX_RATE_AT_TARGET().wMulDown(CURVE_STEEPNESS));
assertLt(
irm.borrowRate(marketParams, market), uint256(int256(irm.MAX_RATE_AT_TARGET()).wMulDown(CURVE_STEEPNESS))
);
}

function _expectedRateAtTarget(Id id, Market memory market) internal view returns (uint256) {
uint256 rateAtTarget = irm.rateAtTarget(id);
int256 speed = ADJUSTMENT_SPEED.wMulDown(_err(market));
uint256 elapsed = (rateAtTarget > 0) ? block.timestamp - market.lastUpdate : 0;
int256 linearVariation = speed * int256(elapsed);
uint256 variationMultiplier = MathLib.wExp(linearVariation);
int256 linearAdaptation = speed * int256(elapsed);
uint256 adaptationMultiplier = MathLib.wExp(linearAdaptation);
return (rateAtTarget > 0)
? rateAtTarget.wMulDown(variationMultiplier).bound(irm.MIN_RATE_AT_TARGET(), irm.MAX_RATE_AT_TARGET())
? rateAtTarget.wMulDown(adaptationMultiplier).bound(irm.MIN_RATE_AT_TARGET(), irm.MAX_RATE_AT_TARGET())
: INITIAL_RATE_AT_TARGET;
}

Expand All @@ -204,42 +210,43 @@ contract AdaptativeCurveIrmTest is Test {
int256 err = _err(market);
int256 speed = ADJUSTMENT_SPEED.wMulDown(err);
uint256 elapsed = (rateAtTarget > 0) ? block.timestamp - market.lastUpdate : 0;
int256 linearVariation = speed * int256(elapsed);
uint256 newRateAtTarget = _expectedRateAtTarget(id, market);
uint256 newBorrowRate = _curve(newRateAtTarget, err);
int256 linearAdaptation = speed * int256(elapsed);
uint256 endRateAtTarget = _expectedRateAtTarget(id, market);
uint256 newBorrowRate = _curve(endRateAtTarget, err);

uint256 avgBorrowRate;
if (linearVariation == 0 || rateAtTarget == 0) {
if (linearAdaptation == 0 || rateAtTarget == 0) {
avgBorrowRate = newBorrowRate;
} else {
// Safe "unchecked" cast to uint256 because linearVariation < 0 <=> newBorrowRate <= borrowRateAfterJump.
// Safe "unchecked" cast to uint256 because linearAdaptation < 0 <=> newBorrowRate <= borrowRateAfterJump.
avgBorrowRate =
uint256((int256(newBorrowRate) - int256(_curve(rateAtTarget, err))).wDivDown(linearVariation));
uint256((int256(newBorrowRate) - int256(_curve(rateAtTarget, err))).wDivDown(linearAdaptation));
}
return avgBorrowRate;
}

function _curve(uint256 rateAtTarget, int256 err) internal pure returns (uint256) {
// Safe "unchecked" cast because err >= -1 (in WAD).
if (err < 0) {
return uint256((WAD - WAD.wDivDown(CURVE_STEEPNESS)).wMulDown(err) + WAD_INT).wMulDown(rateAtTarget);
return uint256(
(WAD_INT - WAD_INT.wDivDown(CURVE_STEEPNESS)).wMulDown(int256(rateAtTarget)).wMulDown(err)
+ int256(rateAtTarget)
);
} else {
return uint256((CURVE_STEEPNESS - WAD).wMulDown(err) + WAD_INT).wMulDown(rateAtTarget);
return
uint256((CURVE_STEEPNESS - WAD_INT).wMulDown(int256(rateAtTarget)).wMulDown(err) + int256(rateAtTarget));
}
}

function _err(Market memory market) internal pure returns (int256) {
function _err(Market memory market) internal pure returns (int256 err) {
if (market.totalSupplyAssets == 0) return -1 ether;
uint256 utilization = market.totalBorrowAssets.wDivDown(market.totalSupplyAssets);

int256 err;
int256 utilization = int256(market.totalBorrowAssets.wDivDown(market.totalSupplyAssets));

if (utilization > TARGET_UTILIZATION) {
// Safe "unchecked" cast because |err| <= WAD.
err = int256((utilization - TARGET_UTILIZATION).wDivDown(WAD - TARGET_UTILIZATION));
err = (utilization - TARGET_UTILIZATION).wDivDown(WAD_INT - TARGET_UTILIZATION);
} else {
// Safe "unchecked" casts because utilization <= WAD and TARGET_UTILIZATION <= WAD.
err = (int256(utilization) - int256(TARGET_UTILIZATION)).wDivDown(int256(TARGET_UTILIZATION));
err = (utilization - TARGET_UTILIZATION).wDivDown(TARGET_UTILIZATION);
}
return err;
}
}

0 comments on commit 7f09d96

Please sign in to comment.