Contract Name:
IRMAdaptiveCurve
Contract Source Code:
// SPDX-License-Identifier: MIT
// Copyright (c) 2023 Morpho Association
pragma solidity ^0.8.0;
import {IIRM} from "evk/InterestRateModels/IIRM.sol";
import {ExpLib} from "./lib/ExpLib.sol";
/// @title IRMAdaptiveCurve
/// @custom:contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/).
/// @author Adapted from Morpho Labs (https://github.com/morpho-org/morpho-blue-irm/).
/// @notice A Linear Kink IRM that adjusts the rate at target utilization based on time spent above/below it.
/// @dev This implementation intentionally leaves variables names, units and ExpLib unchanged from original.
/// Returned rates are extended to RAY per second to be compatible with the EVK.
contract IRMAdaptiveCurve is IIRM {
/// @dev Unit for internal precision.
int256 internal constant WAD = 1e18;
/// @dev Unit for internal precision.
int256 internal constant YEAR = int256(365.2425 days);
/// @notice The name of the IRM.
string public constant name = "IRMAdaptiveCurve";
/// @notice The utilization rate targeted by the model.
/// @dev In WAD units.
int256 public immutable TARGET_UTILIZATION;
/// @notice The initial interest rate at target utilization.
/// @dev In WAD per second units.
/// When the IRM is initialized for a vault this is the rate at target utilization that is assigned.
int256 public immutable INITIAL_RATE_AT_TARGET;
/// @notice The minimum interest rate at target utilization that the model can adjust to.
/// @dev In WAD per second units.
int256 public immutable MIN_RATE_AT_TARGET;
/// @notice The maximum interest rate at target utilization that the model can adjust to.
/// @dev In WAD per second units.
int256 public immutable MAX_RATE_AT_TARGET;
/// @notice The steepness of the interest rate line.
/// @dev In WAD units.
int256 public immutable CURVE_STEEPNESS;
/// @notice The speed at which the rate at target is adjusted up or down.
/// @dev In WAD per second units.
/// For example, with `2e18 / 24 hours` the model will 2x `rateAtTarget` if the vault is fully utilized for a day.
int256 public immutable ADJUSTMENT_SPEED;
/// @notice Internal cached state of the interest rate model.
struct IRState {
/// @dev The current rate at target utilization.
uint144 rateAtTarget;
/// @dev The previous utilization rate of the vault.
int64 lastUtilization;
/// @dev The timestamp of the last update to the model.
uint48 lastUpdate;
}
/// @notice Get the internal cached state of a vault's irm.
mapping(address => IRState) internal irState;
error InvalidParams();
/// @notice Deploy IRMAdaptiveCurve.
/// @param _TARGET_UTILIZATION The utilization rate targeted by the interest rate model.
/// @param _INITIAL_RATE_AT_TARGET The initial interest rate at target utilization.
/// @param _MIN_RATE_AT_TARGET The minimum interest rate at target utilization that the model can adjust to.
/// @param _MAX_RATE_AT_TARGET The maximum interest rate at target utilization that the model can adjust to.
/// @param _CURVE_STEEPNESS The steepness of the interest rate line.
/// @param _ADJUSTMENT_SPEED The speed at which the rate at target utilization is adjusted up or down.
constructor(
int256 _TARGET_UTILIZATION,
int256 _INITIAL_RATE_AT_TARGET,
int256 _MIN_RATE_AT_TARGET,
int256 _MAX_RATE_AT_TARGET,
int256 _CURVE_STEEPNESS,
int256 _ADJUSTMENT_SPEED
) {
// Validate parameters.
if (_TARGET_UTILIZATION <= 0 || _TARGET_UTILIZATION > 1e18) {
revert InvalidParams();
}
if (_INITIAL_RATE_AT_TARGET < _MIN_RATE_AT_TARGET || _INITIAL_RATE_AT_TARGET > _MAX_RATE_AT_TARGET) {
revert InvalidParams();
}
if (_MIN_RATE_AT_TARGET < 0.001e18 / YEAR || _MIN_RATE_AT_TARGET > 10e18 / YEAR) {
revert InvalidParams();
}
if (_MAX_RATE_AT_TARGET < 0.001e18 / YEAR || _MAX_RATE_AT_TARGET > 10e18 / YEAR) {
revert InvalidParams();
}
if (_CURVE_STEEPNESS < 1.01e18 || _CURVE_STEEPNESS > 100e18) {
revert InvalidParams();
}
if (_ADJUSTMENT_SPEED < 2e18 / YEAR || _ADJUSTMENT_SPEED > 1000e18 / YEAR) {
revert InvalidParams();
}
TARGET_UTILIZATION = _TARGET_UTILIZATION;
INITIAL_RATE_AT_TARGET = _INITIAL_RATE_AT_TARGET;
MIN_RATE_AT_TARGET = _MIN_RATE_AT_TARGET;
MAX_RATE_AT_TARGET = _MAX_RATE_AT_TARGET;
CURVE_STEEPNESS = _CURVE_STEEPNESS;
ADJUSTMENT_SPEED = _ADJUSTMENT_SPEED;
}
/// @inheritdoc IIRM
function computeInterestRate(address vault, uint256 cash, uint256 borrows) external returns (uint256) {
if (msg.sender != vault) revert E_IRMUpdateUnauthorized();
int256 utilization = _calcUtilization(cash, borrows);
// If this is the first call then use the current utilization instead of the lastUtilization from storage.
int256 lastUtilization = irState[vault].lastUpdate == 0 ? utilization : irState[vault].lastUtilization;
(uint256 rate, uint256 rateAtTarget) = computeInterestRateInternal(vault, lastUtilization);
irState[vault] = IRState(uint144(rateAtTarget), int64(utilization), uint48(block.timestamp));
return rate * 1e9; // Extend rate to RAY/sec for EVK.
}
/// @inheritdoc IIRM
function computeInterestRateView(address vault, uint256 cash, uint256 borrows) external view returns (uint256) {
int256 utilization = _calcUtilization(cash, borrows);
(uint256 rate,) = computeInterestRateInternal(vault, utilization);
return rate * 1e9; // Extend rate to RAY/sec for EVK.
}
/// @notice Perform computation of the new rate at target without mutating state.
/// @param vault Address of the vault to compute the new interest rate for.
/// @param cash Amount of assets held directly by the vault.
/// @param borrows Amount of assets lent out to borrowers by the vault.
/// @return The new rate at target utilization in RAY units.
function computeRateAtTargetView(address vault, uint256 cash, uint256 borrows) external view returns (uint256) {
int256 utilization = _calcUtilization(cash, borrows);
(, uint256 rateAtTarget) = computeInterestRateInternal(vault, utilization);
return rateAtTarget * 1e9; // Extend rate to RAY/sec for EVK.
}
/// @notice Get the timestamp of the last update for a vault.
/// @param vault Address of the vault to get the last update timestamp for.
/// @return The last update timestamp.
function getLastUpdateTimestamp(address vault) external view returns (uint256) {
return irState[vault].lastUpdate;
}
/// @notice Compute the new interest rate and rate at target utilization of a vault.
/// @param vault Address of the vault to compute the new interest rate for.
/// @return The new interest rate at current utilization.
/// @return The new interest rate at target utilization.
function computeInterestRateInternal(address vault, int256 utilization) internal view returns (uint256, uint256) {
// Calculate the normalized distance between current utilization and target utilization.
// `err` is normalized to [-1, +1] where -1 is 0% util, 0 is at target and +1 is 100% util.
int256 errNormFactor = utilization > TARGET_UTILIZATION ? WAD - TARGET_UTILIZATION : TARGET_UTILIZATION;
int256 err = (utilization - TARGET_UTILIZATION) * WAD / errNormFactor;
IRState memory state = irState[vault];
int256 startRateAtTarget = int256(uint256(state.rateAtTarget));
int256 endRateAtTarget;
if (startRateAtTarget == 0) {
// First interaction.
endRateAtTarget = INITIAL_RATE_AT_TARGET;
} else {
// The speed is assumed constant between two updates, but it is in fact not constant because of interest.
// So the rate is always underestimated.
int256 speed = ADJUSTMENT_SPEED * err / WAD;
// Calculate the adaptation parameter.
int256 elapsed = int256(block.timestamp - state.lastUpdate);
int256 linearAdaptation = speed * elapsed;
if (linearAdaptation == 0) {
endRateAtTarget = startRateAtTarget;
} else {
endRateAtTarget = _newRateAtTarget(startRateAtTarget, linearAdaptation);
}
}
return (uint256(_curve(endRateAtTarget, err)), uint256(endRateAtTarget));
}
/// @notice Calculate the interest rate according to the linear kink model.
/// @param rateAtTarget The current interest rate at target utilization.
/// @param err The distance between the current utilization and the target utilization, normalized to `[-1, +1]`.
/// @dev rate = ((1-1/C)*err + 1) * rateAtTarget if err < 0
/// (C-1)*err + 1) * rateAtTarget else.
/// @return The new interest rate at current utilization.
function _curve(int256 rateAtTarget, int256 err) internal view returns (int256) {
// Non negative because 1 - 1/C >= 0, C - 1 >= 0.
int256 coeff = err < 0 ? WAD - WAD * WAD / CURVE_STEEPNESS : CURVE_STEEPNESS - WAD;
// Non negative if rateAtTarget >= 0 because if err < 0, coeff <= 1.
return ((coeff * err / WAD) + WAD) * rateAtTarget / WAD;
}
/// @notice Calculate the new interest rate at target utilization by applying an adaptation.
/// @param startRateAtTarget The current interest rate at target utilization.
/// @param linearAdaptation The adaptation parameter, used as a power of `e`.
/// @dev Applies exponential growth/decay to the current interest rate at target utilization.
/// Formula: `rateAtTarget = startRateAtTarget * e^linearAdaptation` bounded to min and max.
/// @return The new interest rate at target utilization.
function _newRateAtTarget(int256 startRateAtTarget, int256 linearAdaptation) internal view returns (int256) {
int256 rateAtTarget = startRateAtTarget * ExpLib.wExp(linearAdaptation) / WAD;
if (rateAtTarget < MIN_RATE_AT_TARGET) return MIN_RATE_AT_TARGET;
if (rateAtTarget > MAX_RATE_AT_TARGET) return MAX_RATE_AT_TARGET;
return rateAtTarget;
}
/// @notice Calculate the utilization rate, given cash and borrows from the vault.
/// @param cash Amount of assets held directly by the vault.
/// @param borrows Amount of assets lent out to borrowers by the vault.
/// @return The utilization rate in WAD.
function _calcUtilization(uint256 cash, uint256 borrows) internal pure returns (int256) {
int256 totalAssets = int256(cash + borrows);
if (totalAssets == 0) return 0;
return int256(borrows) * WAD / totalAssets;
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.8.0;
/// @title IIRM
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Interface of the interest rate model contracts used by EVault
interface IIRM {
error E_IRMUpdateUnauthorized();
/// @notice Perform potentially state mutating computation of the new interest rate
/// @param vault Address of the vault to compute the new interest rate for
/// @param cash Amount of assets held directly by the vault
/// @param borrows Amount of assets lent out to borrowers by the vault
/// @return Then new interest rate in second percent yield (SPY), scaled by 1e27
function computeInterestRate(address vault, uint256 cash, uint256 borrows) external returns (uint256);
/// @notice Perform computation of the new interest rate without mutating state
/// @param vault Address of the vault to compute the new interest rate for
/// @param cash Amount of assets held directly by the vault
/// @param borrows Amount of assets lent out to borrowers by the vault
/// @return Then new interest rate in second percent yield (SPY), scaled by 1e27
function computeInterestRateView(address vault, uint256 cash, uint256 borrows) external view returns (uint256);
}
// SPDX-License-Identifier: MIT
// Copyright (c) 2023 Morpho Association
pragma solidity ^0.8.0;
/// @title ExpLib
/// @custom:contact [email protected]
/// @author Adapted from Morpho Labs
/// (https://github.com/morpho-org/morpho-blue-irm/blob/a824ce06a53f45f12d0ffedb51abd756896b29fa/src/adaptive-curve-irm/libraries/ExpLib.sol)
/// @notice Library to approximate the exponential function.
library ExpLib {
int256 internal constant WAD = 1e18;
/// @dev ln(2).
int256 internal constant LN_2_INT = 0.693147180559945309e18;
/// @dev ln(1e-18).
int256 internal constant LN_WEI_INT = -41.446531673892822312e18;
/// @dev Above this bound, `wExp` is clipped to avoid overflowing when multiplied with 1e18.
/// @dev This upper bound corresponds to: ln(type(int256).max / 1e36) (scaled by WAD, floored).
int256 internal constant WEXP_UPPER_BOUND = 93.859467695000404319e18;
/// @dev The value of wExp(`WEXP_UPPER_BOUND`).
int256 internal constant WEXP_UPPER_VALUE = 57716089161558943949701069502944508345128.422502756744429568e18;
/// @dev Returns an approximation of exp.
function wExp(int256 x) internal pure returns (int256) {
unchecked {
// If x < ln(1e-18) then exp(x) < 1e-18 so it is rounded to zero.
if (x < LN_WEI_INT) return 0;
// `wExp` is clipped to avoid overflowing when multiplied with 1e18.
if (x >= WEXP_UPPER_BOUND) return WEXP_UPPER_VALUE;
// Decompose x as x = q * ln(2) + r with q an integer and -ln(2)/2 <= r <= ln(2)/2.
// q = x / ln(2) rounded half toward zero.
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) - x| <= ln(2)/2.
int256 r = x - q * LN_2_INT;
// Compute e^r with a 2nd-order Taylor polynomial.
// Safe unchecked because |r| < 1e18.
int256 expR = WAD + r + (r * r) / WAD / 2;
// Return e^x = 2^q * e^r.
if (q >= 0) return expR << uint256(q);
else return expR >> uint256(-q);
}
}
}