Dynamic Interest Rate Model
Overview
The Dynamic Interest Rate Model is a sophisticated interest rate mechanism designed to efficiently balance supply and demand within Curvance lending markets. It builds upon traditional "jump rate" models while introducing dynamic elements that automatically respond to changing market conditions.
Core Components
Interest Rate Structure
The model uses two primary interest rate components:
Base Interest Rate: Applied linearly as utilization increases from 0% to the vertex point, which increases linearly until a certain utilization threshold (
vertexStartingPoint
) is reached.Vertex Interest Rate: Applied when utilization exceeds the vertex point, multiplied by the Vertex Multiplier, which adjusts more steeply based on liquidity utilization.
Vertex Starting Point: The utilization threshold where the model switches from base to vertex rate.
Dynamic Vertex Multiplier
The heart of the model is the Vertex Multiplier - a dynamic coefficient that adjusts based on market conditions:
Default Value: Starts at 1.0 (WAD).
Maximum Value: Capped at vertexMultiplierMax.

Data Flow
Market Utilization → Calculated from Debt/(AssetsHeld + Debt).
Utilization → Drives interest rate calculations through base and vertex formulas.
Interest Rates → Applied to borrowers and distributed to lenders (minus protocol fees).
Vertex Multiplier → Adjusted on each call by the linked token; cadence is ADJUSTMENT_RATE.
Decay Mechanism → Continuously reduces elevated multiplier values over time.
State Machine
The Vertex Multiplier operates as a state machine with the following transitions:
States and Transitions
Initialization State
Starting point: vertexMultiplier = WAD (1.0).
Next update timestamp set.
Adjustment States
Based on utilization relative to thresholds:
Below decreaseThresholdMax: Maximum negative adjustment + decay.
Below vertex but above decreaseThresholdMax: Scaled negative adjustment + decay.
Above vertex but below increaseThreshold: Only decay applied.
Above increaseThreshold: Positive adjustment + decay.
Transition Conditions
Updates occur when the linked token calls the model; the model returns ADJUSTMENT_RATE as the cadence hint.
Rate update only possible when properly linked to a BorrowableCToken.
Decay Mechanism
The decay feature introduces a downward sloping characteristic:
When the multiplier is elevated, a constant negative velocity is applied.
This occurs regardless of positive/negative acceleration from utilization.
If the multiplier is elevated due to high utilization, it naturally decays over time to prevent interest rates from remaining too high indefinitely.
Decay subtracts a fraction of the current multiplier each adjustment, pulling it downward toward 1.0 (floored at WAD).
Creates natural incentives for borrowers while protecting against liquidity crunches.

Configuration Parameters
The model is governed by several parameters that define its behavior:
Base Parameters:
baseRatePerSecond: Rate at which interest is accumulated, before
vertexStart
.vertexInterestRate: Rate at which interest is accumulated, after
vertexStart
.vertexStart: The utilization point where vertex takes effect.
Adjustment Controls:
ADJUSTMENT_RATE: Fixed time between multiplier updates in seconds.
adjustmentVelocity: Maximum rate of multiplier change per update.
vertexMultiplierMax: Maximum allowed value for multiplier.
Threshold Parameters:
increaseThresholdStart: Point above vertex where multiplier increases.
decreaseThresholdEnd: Point above vertex where multiplier decreases.
decayRate: Rate at which elevated multipliers naturally decrease.
Design Benefits
Responsive to Market Conditions:
High utilization leads to increased rates, attracting lenders.
Sustained high rates encourage borrowers to repay.
Self-Balancing:
Creates a feedback loop that stabilizes market liquidity.
Prevents liquidity crunches through predictive rate adjustments.
Growth Incentives:
Decay mechanism helps maintain competitive rates during normal operations.
Creates naturally decreasing interest rates in stable markets.
Gas Optimization:
Uses bit-packed storage for multiplier and timestamp.
Efficient math calculations for model computation.
Practical Example
If a market experiences sustained high utilization:
Interest rates will gradually increase as the Vertex Multiplier rises.
This attracts new lenders while encouraging borrowers to repay.
As utilization decreases, rates begin to fall (but not immediately due to the multiplier).
The decay mechanism ensures rates will eventually normalize even without full utilization correction.
This creates a more stable, responsive system compared to fixed-rate models while protecting the protocol from potential liquidity crises.
Math Equations
Constants:
WAD = 1e18
BPS = 10_000
WAD_BPS = 1e22
Utilization Rate Calculation
function utilizationRate(
uint256 assetsHeld,
uint256 debt
) public pure returns (uint256 r) {
// Utilization rate is 0 when there are no outstanding debt.
r = debt == 0 ? 0 : _mulDiv(debt, WAD, assetsHeld + debt);
}
Base Interest Rate Calculation
function _baseRate(
uint256 util,
uint256 baseRatePerSecond
) internal pure returns (uint256 r) {
r = _mulDiv(util, baseRatePerSecond, WAD);
}
Vertex Interest Rate Calculation
function _vertexRate(
uint256 util,
uint256 baseRatePerSecond,
uint256 vertexRatePerSecond,
uint256 vertexStart,
uint256 multiplier
) internal pure returns (uint256 r) {
r = _mulDiv(vertexStart, baseRatePerSecond, WAD) +
_mulDiv(
util - vertexStart,
vertexRatePerSecond * multiplier,
WAD_SQUARED
);
}
Final Borrow Interest Rate Calculation
function borrowRate(
uint256 assetsHeld,
uint256 debt
) public view returns (uint256 r) {
uint256 util = utilizationRate(assetsHeld, debt);
// Cache from storage since we only need to query a new config values.
RatesConfig storage c = ratesConfig;
uint256 vertexStart = c.vertexStart;
if (util <= vertexStart) {
return _baseRate(util, c.baseRatePerSecond);
}
r = _vertexRate(
util,
c.baseRatePerSecond,
c.vertexRatePerSecond,
vertexStart,
vertexMultiplier
);
}
Vertex Multiplier Adjustment (Above Vertex)
decay:
start (converted to WAD):
Case 1: Low Utilization (utilization <= start)
Case 2: High Utilization (utilization > start)
function _updateAboveVertex(
RatesConfig memory c,
uint256 util,
uint256 multiplier
) internal pure returns (uint256 newMultiplier) {
// Calculate decay rate.
uint256 decay = _mulDiv(multiplier, c.decayPerAdjustment, BPS);
if (util <= (1e14 * uint256(c.increaseThresholdStart))) {
newMultiplier = multiplier - decay;
// Check if decay rate sends new rate below 1.
return newMultiplier < WAD ? WAD : newMultiplier;
}
// Apply a positive multiplier to the current multiplier based on
// `util` vs `increaseThresholdStart` and `WAD`.
// Then apply decay effect.
newMultiplier = _positiveShift(
multiplier, // `multiplier` in `WAD`.
c.adjustmentVelocity, // `adjustmentVelocity` in `BPS`.
decay, // `decay` in `multiplier` aka `WAD`.
util, // `current` in `WAD`.
1e14 * uint256(c.increaseThresholdStart) // `start` convert to WAD to match util.
);
// Update and return with adjustment and decay rate applied.
// Its theorectically possible for the multiplier to be below 1
// due to decay, so we need to check like in below vertex.
if (newMultiplier < WAD) {
return WAD;
}
// Make sure `newMultiplier` is not above `vertexMultiplierMax`.
newMultiplier = newMultiplier < c.vertexMultiplierMax
? newMultiplier
: c.vertexMultiplierMax;
}
function _positiveShift(
uint256 multiplier,
uint256 adjustmentVelocity,
uint256 decay,
uint256 current, // `util`.
uint256 start // `increaseThresholdStart`.
) internal pure returns (uint256 result) {
// We do not need to check for current >= end, since we know util is
// the absolute maximum utilization is 100%, and thus current == end.
// Which will result in WAD result for `shift`.
// Thus, this will be bound between [0, WAD].
uint256 shift = _mulDiv(current - start, WAD, WAD - start);
// Apply `shift` result to adjustment velocity.
// Then add 100% on top for final adjustment value to `multiplier`.
// We use WAD_BPS here since `shift` is in `WAD` for max precision but
// `adjustmentVelocity` is in BPS since it does not need greater
// precision due to being configured in `BPS`.
shift = WAD_BPS + (shift * adjustmentVelocity);
// Apply positive `shift` effect to `currentMultiplier`, and
// adjust for 1e36 precision. Then apply decay effect.
result = _mulDiv(multiplier, shift, WAD_BPS) - decay;
}
Vertex Multiplier Adjustment (Below Vertex)
decay:
end (converted to WAD):
Case 1: Very Low Utilization (utilization <= end)
Case 2: Moderate Utilization (utilization > end)
function _updateBelowVertex(
RatesConfig memory c,
uint256 util,
uint256 multiplier
) internal pure returns (uint256 newMultiplier) {
// Calculate decay rate.
uint256 decay = _mulDiv(multiplier, c.decayPerAdjustment, BPS);
// Convert `decreaseThresholdEnd` to `WAD` to be in same terms as
// `util`, no precision loss as a result.
if (util <= (1e14 * uint256(c.decreaseThresholdEnd))) {
// Apply maximum adjustVelocity reduction (shift = 1).
// We only need to adjust for `BPS` precision since `shift`
// is not used here.
// currentMultiplier / (1 + adjustmentVelocity) = newMultiplier.
newMultiplier = _mulDiv(
multiplier,
BPS,
BPS + c.adjustmentVelocity
) - decay;
// Check if decay rate sends multiplier below 1.
return newMultiplier < WAD ? WAD : newMultiplier;
}
// Apply a negative multiplier to the current multiplier based on
// `util` vs `vertexStart` and `decreaseThresholdEnd`.
// Then apply decay effect.
newMultiplier = _negativeShift(
multiplier, // `multiplier` in `WAD`.
c.adjustmentVelocity, // `adjustmentVelocity` in `BPS`.
decay, // `decay` in `multiplier` aka `WAD`.
util, // `current` in `WAD`.
c.vertexStart, // `start` in `WAD`.
1e14 * uint256(c.decreaseThresholdEnd) // `end` convert to WAD to match util/vertexStart.
);
// Update and return with adjustment and decay rate applied.
// But first check if new rate sends multiplier below 1.
return newMultiplier < WAD ? WAD : newMultiplier;
}
function _negativeShift(
uint256 multiplier,
uint256 adjustmentVelocity,
uint256 decay,
uint256 current, // `util`.
uint256 start, // `vertexStart`.
uint256 end // `decreaseThresholdEnd`.
) internal pure returns (uint256 result) {
// Calculate linear curve multiplier. We know that current > end,
// based on pre conditional checks.
// Thus, this will be bound between [0, WAD].
uint256 shift = _mulDiv(start - current, WAD, start - end);
// Apply `shift` result to adjustment velocity.
// Then add 100% on top for final adjustment value to `multiplier`.
// We use WAD_BPS here since `shift` is in `WAD` for max precision but
// `adjustmentVelocity` is in BPS since it does not need greater
// precision due to being configured in `BPS`.
shift = WAD_BPS + (shift * adjustmentVelocity);
// Apply negative `shift` effect to `currentMultiplier`, and
// adjust for 1e36 precision. Then apply decay effect.
result = _mulDiv(multiplier, WAD_BPS, shift) - decay;
}
User Interaction Functions
predictedBorrowRate()
Contract: DynamicIRM
Description: Calculates the current borrow rate per second with updated vertex multiplier applied, predicting what the rate will be after the next adjustment.
Function signature:
function predictedBorrowRate(
uint256 assetsHeld,
uint256 debt
) public view returns (uint256 result)
uint256
assetsHeld
The amount of underlying assets held in the pool.
uint256
debt
The amount of outstanding debt in the pool.
Return data:
uint256
The borrow rate percentage per second, in WAD
.
borrowRate()
Contract: DynamicIRM
Description: Calculates the current borrow rate per second based on current market conditions.
Function signature:
function borrowRate(
uint256 assetsHeld,
uint256 debt
) public view returns (uint256 r)
uint256
assetsHeld
The amount of underlying assets held in the pool.
uint256
debt
The amount of outstanding debt in the pool.
Return data:
uint256
The borrow interest rate percentage, per second, in WAD
.
supplyRate()
Contract: DynamicIRM
Description: Calculates the current supply rate, per second. This function's intention is for frontend data querying and should not be used for onchain execution.
Function signature:
function borrowRate(
uint256 assetsHeld,
uint256 debt
) public view returns (uint256 r)
uint256
assetsHeld
The amount of underlying assets held in the pool.
uint256
debt
The amount of outstanding debt in the pool.
uint256
interestFee
The current interest rate protocol fee for the market token, in BPS
.
Return data:
uint256
The borrow interest rate percentage, per second, in WAD
.
utilizationRate()
Contract: DynamicIRM
Description: Calculates the utilization rate of the market using formula: debt / (assetsHeld + debt).
Function signature:
function borrowRate(
uint256 assetsHeld,
uint256 debt
) public view returns (uint256 r)
uint256
assetsHeld
The amount of underlying assets held in the pool.
uint256
debt
The amount of outstanding debt in the pool.
Return data:
uint256
The utilization rate between [0, WAD].
vertexMultiplier()
Contract: DynamicIRM
Description: The dynamic value applied to vertexRatePerSecond
, increasing it as utilizationRate
remains elevated over time, adjusted every ADJUSTMENT_RATE
, in WAD
.
Function signature:
// uint256 public vertexMultiplier;
function vertexMultiplier() public view returns (uint256);
Return data:
uint256
The utilization rate between [0, WAD].
linkedToken()
Contract: DynamicIRM
Description: The borrowable Curvance token linked to this interest rate model contract.
Function signature:
function linkedToken() external view returns (address result);
Return data:
uint256
The linked borrowableCToken address.
Last updated