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

  1. Market Utilization → Calculated from Debt/(AssetsHeld + Debt).

  2. Utilization → Drives interest rate calculations through base and vertex formulas.

  3. Interest Rates → Applied to borrowers and distributed to lenders (minus protocol fees).

  4. Vertex Multiplier → Adjusted on each call by the linked token; cadence is ADJUSTMENT_RATE.

  5. 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

  1. Responsive to Market Conditions:

    1. High utilization leads to increased rates, attracting lenders.

    2. Sustained high rates encourage borrowers to repay.

  2. Self-Balancing:

    1. Creates a feedback loop that stabilizes market liquidity.

    2. Prevents liquidity crunches through predictive rate adjustments.

  3. Growth Incentives:

    1. Decay mechanism helps maintain competitive rates during normal operations.

    2. Creates naturally decreasing interest rates in stable markets.

  4. Gas Optimization:

    1. Uses bit-packed storage for multiplier and timestamp.

    2. Efficient math calculations for model computation.

Practical Example

If a market experiences sustained high utilization:

  1. Interest rates will gradually increase as the Vertex Multiplier rises.

  2. This attracts new lenders while encouraging borrowers to repay.

  3. As utilization decreases, rates begin to fall (but not immediately due to the multiplier).

  4. 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

  1. Utilization Rate Calculation

UtilizationRate=Debt/(AssetsHeld+Debt)Utilization Rate = Debt / (AssetsHeld + Debt)
    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);
    }

  1. Base Interest Rate Calculation

BaseRate=(UtilizationbaseRatePerSecond)/WADBase Rate = (Utilization * baseRatePerSecond) / WAD
    function _baseRate(
        uint256 util,
        uint256 baseRatePerSecond
    ) internal pure returns (uint256 r) {
        r = _mulDiv(util, baseRatePerSecond, WAD);
    }

  1. Vertex Interest Rate Calculation

VertexInterestRate=((vertexStartbaseRatePerSecond)/WAD)+((utilizationvertexStart)vertexRatePerSecondvertexMultiplier)/WAD2VertexInterestRate = ((vertexStart * baseRatePerSecond) / WAD) + ((utilization - vertexStart) * vertexRatePerSecond * vertexMultiplier) / WAD^2
    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
        );
    }

  1. Final Borrow Interest Rate Calculation

BorrowRate=BaseInterestRate+VertexInterestRateBorrow Rate = Base Interest Rate + Vertex Interest Rate
    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
        );
    }

  1. Vertex Multiplier Adjustment (Above Vertex)

decay:

decay=(multiplierdecayPerAdjustment)/BPSdecay = (multiplier * decayPerAdjustment) / BPS

start (converted to WAD):

start=increaseThresholdStart1e14start = increaseThresholdStart * 1e14

Case 1: Low Utilization (utilization <= start)

newMultiplier=max((multiplierdecay),WAD)newMultiplier = max((multiplier - decay), WAD)

Case 2: High Utilization (utilization > start)

shift=((utilstart)WAD)/(WADstart)shift = ((util - start ) * WAD) / (WAD - start)
adjustedShift=WADBPS+shiftadjustmentVelocity)adjustedShift = WADBPS+ shift * adjustmentVelocity)
newMultiplier=(multiplieradjustedShift)/WADBPSdecaynewMultiplier = (multiplier * adjustedShift) / WADBPS - decay
finalResult=min(max(newMultiplier,WAD),vertexMultiplierMax)finalResult = min(max(newMultiplier, WAD), vertexMultiplierMax)
    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;
    }

  1. Vertex Multiplier Adjustment (Below Vertex)

decay:

decay=(multiplierdecayPerAdjustment)/BPSdecay = (multiplier * decayPerAdjustment) / BPS

end (converted to WAD):

end=decreaseThresholdEnd1e14end = decreaseThresholdEnd * 1e14

Case 1: Very Low Utilization (utilization <= end)

newMultiplier=max((multiplierBPS)/(BPS+adjustmentVelocity)decay,WAD)newMultiplier = max((multiplier * BPS) / (BPS + adjustmentVelocity) - decay, WAD)

Case 2: Moderate Utilization (utilization > end)

shift=((vertexStartutil)WAD/(vertexStartend)shift = ((vertexStart - util) * WAD / (vertexStart - end)
adjustedShift=WADBPS+(shiftadjustmentVelocity)adjustedShift = WADBPS + (shift * adjustmentVelocity)
newMultiplier=(multiplierWADBPS)/adjustedShiftdecaynewMultiplier = (multiplier * WADBPS) / adjustedShift - decay
result=max(newMultiplier,WAD)result = max(newMultiplier, WAD)

    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)
Type
Name
Description

uint256

assetsHeld

The amount of underlying assets held in the pool.

uint256

debt

The amount of outstanding debt in the pool.

Return data:

Type
Description

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)
Type
Name
Description

uint256

assetsHeld

The amount of underlying assets held in the pool.

uint256

debt

The amount of outstanding debt in the pool.

Return data:

Type
Description

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)
Type
Name
Description

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:

Type
Description

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)
Type
Name
Description

uint256

assetsHeld

The amount of underlying assets held in the pool.

uint256

debt

The amount of outstanding debt in the pool.

Return data:

Type
Description

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:

Type
Description

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:

Type
Description

uint256

The linked borrowableCToken address.


Last updated