Interest Rate Model (IRM)
Moolah markets delegate borrow-rate computation to a pluggable Interest Rate Model (IRM). Each market stores an irm address, and on every interest accrual Moolah calls that contract to obtain the current borrow rate.
Two IRM implementations ship with the protocol:
InterestRateModel
Adaptive-curve rate as a function of utilization, with a rate-at-target that adapts over time
Standard utilization-based markets
FixedRateIrm
Administratively set fixed per-market rate
Fixed term / fixed rate broker products (see Integration Patterns)
Every IRM implements the shared IIrm interface:
interface IIrm {
/// @notice Borrow rate per second (scaled by WAD); may modify storage.
function borrowRate(MarketParams memory marketParams, Market memory market) external returns (uint256);
/// @notice Borrow rate per second (scaled by WAD); read-only.
function borrowRateView(MarketParams memory marketParams, Market memory market) external view returns (uint256);
}All rates in the IRM are expressed per second, scaled by WAD (1e18). Moolah compounds this per-second rate over the elapsed time between interactions when accruing interest.
Adaptive Curve Model
InterestRateModel computes the borrow rate as a curve function of utilization around a target utilization. The curve is anchored by a rateAtTarget value that itself adapts over time: when utilization sits above target, rateAtTarget drifts upward; when below target, it drifts downward. This pushes the market back toward the target utilization without governance intervention.
Utilization and error
Utilization is total borrows divided by total supply:
The model measures how far utilization is from target as a normalized error err, mapped so that err = -1 at zero utilization, err = 0 at target utilization, and err = +1 at full (100%) utilization:
The curve
Given the current rateAtTarget and the error, the instantaneous rate is a piecewise-linear curve. Below target the rate is scaled down toward rateAtTarget / CURVE_STEEPNESS; above target it is scaled up toward rateAtTarget * CURVE_STEEPNESS:
where C = CURVE_STEEPNESS. At err = 0 (utilization exactly at target) the borrow rate equals rateAtTarget.
Adaptation of rateAtTarget
rateAtTarget is stored per market (mapping(Id => int256) rateAtTarget) and is updated only on stateful interactions. Between two updates it moves according to ADJUSTMENT_SPEED * err per second, applied through a continuous-compounding (exponential) adaptation over the elapsed time:
The second clamp is applied to the value that is actually persisted, so after a rateCap or rateFloor change the stored rateAtTarget is bounded to [rateFloor * 4, cap / 4] — not only to the protocol-wide MIN_RATE_AT_TARGET / MAX_RATE_AT_TARGET. The factor of 4 is CURVE_STEEPNESS, so the bound expresses the cap/floor in terms of the curve endpoints. If rateFloor * 4 exceeds cap / 4, the cap side wins.
The average rate returned to Moolah over the elapsed interval is approximated with a trapezoidal rule (using the start, mid, and end rateAtTarget), so the reported rate reflects the whole interval rather than only its endpoints. On a market's first interaction (rateAtTarget == 0), the model seeds both the average and end rate-at-target with INITIAL_RATE_AT_TARGET.
The resulting average rate is additionally bounded by a per-market cap and floor (rateCap, rateFloor) and a protocol-wide minimum cap (minCap); when no explicit cap is set, DEFAULT_RATE_CAP applies. These bounds are manager/bot-adjustable on-chain values.
Changing either bound settles first: updateRateCap and updateRateFloor (on both IRMs, and setBorrowRate on FixedRateIrm) call Moolah.accrueInterest for the market before writing the new value, so interest accrued under the old bound is booked at the old bound. The call is a no-op for a market that has never been touched (lastUpdate == 0).
Adaptive-curve constants
These are compiled into ConstantsLib and inlined, so there is no getter to read them from — changing one requires a contract upgrade. All rate constants are per second, WAD-scaled; the percentages in Meaning are those same values expressed per year.
CURVE_STEEPNESS
4 ether (4e18)
Curve steepness C = 4. Rate ranges from rateAtTarget / 4 at 0% utilization to rateAtTarget * 4 at 100%.
TARGET_UTILIZATION
0.9 ether (0.9e18)
Target utilization = 90%.
ADJUSTMENT_SPEED
50 ether / 365 days
Adaptation speed; rateAtTarget moves at ADJUSTMENT_SPEED * err per second (≈ 50 / year at full error).
INITIAL_RATE_AT_TARGET
0.04 ether / 365 days
Seed rate-at-target on first interaction; 4% APR-equivalent at target (rate spans ~1%–16% across the curve).
MIN_RATE_AT_TARGET
0.001 ether / 365 days
Lower clamp on rateAtTarget; 0.1% at target (curve minimum ~0.025%).
MAX_RATE_AT_TARGET
2.0 ether / 365 days
Upper clamp on rateAtTarget; 200% at target (curve maximum ~800%).
DEFAULT_RATE_CAP
uint256(0.3 ether) / 365 days
Default per-second cap on the returned borrow rate (30% annualized) when a market has no explicit rateCap.
borrowRateView vs borrowRate
Both entry points evaluate the same adaptive-curve math, but differ in whether they persist the updated rateAtTarget:
borrowRateView
borrowRate
Mutability
view (no state change)
State-changing
Caller
Anyone
MOOLAH only (reverts otherwise)
Effect
Computes the average rate for the current market snapshot without writing
Computes the rate, writes rateAtTarget[id] = endRateAtTarget, emits BorrowRateUpdate
Use case
Off-chain quoting, simulations, front-ends
Called by Moolah during interest accrual on supply / borrow / repay / etc.
Because borrowRate requires msg.sender == MOOLAH, integrators reading a rate directly should call borrowRateView. The value it returns is the average per-second rate that would be applied for the given market snapshot; it does not advance rateAtTarget. borrowRateView uses the stored rateAtTarget and the elapsed time implied by market.lastUpdate, so its result already reflects adaptation that has not been committed on-chain.
Reading the current rate on-chain
Two complementary reads are available:
The anchor rate.
rateAtTarget(Id id)returns the stored per-second rate-at-target for a market (the height of the curve). It is seeded toINITIAL_RATE_AT_TARGET, clamped by the market's floor and cap, in the market-creation transaction —createMarketcallsIIrm.borrowRateonce to initialize the model. A0reading therefore means this IRM has never been invoked for thatId: the market does not exist, or it uses a different IRM.The current borrow rate. Fetch the market's live
MarketParamsandMarketstructs from Moolah, then callborrowRateView:
Per-second → APY conversion
The returned rate is a per-second rate scaled by WAD. Moolah accrues interest with continuous (per-second Taylor-compounded) growth, so the borrow APY is:
The supply APY is lower than the borrow APY, scaled by utilization and net of the market fee; it is not returned by the IRM and must be derived from market state.
FixedRateIrm
For products that do not use the utilization-based curve, brokers can point a market at FixedRateIrm. Instead of computing a rate from utilization, it returns a per-market rate that has been set administratively.
MAX_BORROW_RATE
8.0 ether / 365 days
Maximum settable per-second rate (800% annualized).
borrowRateStored(Id id)
int256
The fixed per-second rate stored for a market.
setBorrowRate(Id id, int256 newBorrowRate)
—
Sets the fixed rate. Access-controlled (bot role); must be >= 0 and <= MAX_BORROW_RATE, and within any configured rateCap / rateFloor. Interest accrued under the previous rate is settled first (see below).
borrowRateView(...) / borrowRate(...)
uint256
Both return the stored rate (clamped to MAX_BORROW_RATE, then to the market cap and floor). Unlike the adaptive model, borrowRate here is also view and holds no adaptive state. A market whose rate has never been set has borrowRateStored == 0, so these return 0 (subject to any configured floor) rather than reverting.
A 0 rate means no interest accrues, so treat an unset market as unconfigured rather than free. This IRM backs the fixed term / fixed rate broker products described in Integration Patterns.
Related
Contract & Interface Reference —
minLoan, the global reentrancy guard and the rest of the Lista-specific controls.Smart Contract — deployed contract addresses.
Last updated
Was this helpful?