Interest model
This page covers how trove debt accrues over time, how the borrow rate is composed, and how shielded troves are priced. The contracts involved are InterestEngine (per branch), Aggregator (per-branch delta-rate offsets), and RateParControl (the system rate).
The two accumulators
Each branch's InterestEngine maintains two per-second accumulators with 1e27 precision:
| Accumulator | What it accrues | Who pays it |
|---|---|---|
accumulatedRate | The branch's effective interest rate | All troves on the branch |
accumulatedShieldRate | An additional component for shielded troves | Shielded troves only |
A trove stores its debt as a normalized value (debt at deployment time, in 1e18 RD units) plus a snapshot of the accumulator(s) at the moment of its last interaction. Actual current debt is:
unshielded:
actualDebt = normalizedDebt × accumulatedRate / 1e27
shielded:
actualDebt = normalizedDebt × (accumulatedRate + premium component) / 1e27
The exact composition is in InterestEngine.sol; the user-relevant fact is that interest accrues continuously, and drip() simply moves the accumulator forward by the elapsed seconds × current rate.
The effective per-branch rate
The branch's per-second rate accumulator is composed additively in the per-second-delta domain, then clamped to a final per-branch range. Working in per-second-delta units (i.e., subtracting the multiplicative identity RATE_PRECISION = 1e27):
branch_delta = (system_rate − 1) + branch_offset
branch_rate = 1 + clamp(branch_delta, MIN_FINAL_DELTA, MAX_FINAL_DELTA)
In APR-thinking shortcut:
branch_APR ≈ clamp(system_APR + branch_offset_APR, 0.25%, 50%)
This is the exact composition in Aggregator._calcCollateralRate:
int256 deltaInterestRate = int256(_interestRate - RATE_PRECISION) + _offset;
return RATE_PRECISION + _clipFinalDelta(deltaInterestRate);
The components:
system_rate: output ofRateParControl. Same number across the protocol. Bounded[0.1% APR, 30% APR], biased at2% APR. See Peg & PI controller.branch_offset: signed output of theAggregator's per-branch PI loop on debt utilization. Positive when the branch is above its issuance target (hot); can be negative when below target, subject to the final-rate clamp keepingbranch_APR ≥ 0.25%.- Final-rate clamps:
MIN_FINAL_DELTA ≈ 0.25% APR,MAX_FINAL_DELTA ≈ 50% APR. These bind on the final branch rate after the offset is added, note that the upper clamp (50%) is higher than the system-rate's 30% ceiling, giving stressed branches additional headroom; the lower clamp (0.25%) is higher than the system-rate's 0.1% floor, so a deeply-negative offset is bounded.
The Aggregator updates the offset based on a 72-hour-half-life EMA of branch debt vs. a target. When the branch is utilizing more than its target share, the offset climbs. The drip interval is 2 hours (with up to 8 hours of catch-up).
Realized APR ranges
A few representative slices. Numbers from the protocol-repo reference (docs/collateral-rate-and-borrow-rate-reference.md).
At system_rate = 2% APR base, 6-hour update window:
| Debt-utilization error | Unshielded APR |
|---|---|
| −100% to 0% | clamped at minimum (≈ 0.25%) |
| 0% (neutral) | ~2.00% |
| +5% | ~2.43% |
| +10% | ~2.86% |
| +20% | ~3.73% |
| +50% | ~6.37% |
| +100% | ~10.94% |
At system_rate = 2.5% APR base, same window: starts ~2.5% at neutral, peaks ~14% at +100% utilization.
At a higher base (controller responding to RD trading below par), all rows shift up.
Shield pricing
Shield pricing produces one signed adjustment off the branch rate, applied with opposite signs to the two books. From InterestEngine._calcRates:
unShieldedRate = _collRate.sub(v); // unshielded pays branch_rate − v
shieldedRate = _collRate.add(u); // shielded pays branch_rate + u
u and v are both non-negative quantities derived from the same shield-share curve. So:
- A shielded trove pays a positive shield-share adjustment (
+u). Call this the shield surcharge. - An unshielded trove receives a negative shield-share adjustment (
−v). Call this the unshielded discount.
The two are halves of the same mechanism, the shielded book's surcharge funds the unshielded book's discount. If you prefer to call the whole thing the "shield premium," that's fine, but note the premium is signed: positive for shielded troves, negative for unshielded troves.
The curve is designed so that:
- The shielded book subsidizes the unshielded book. Every shielded surcharge corresponds to an unshielded discount.
- The unshielded rate can't go below a floor. Even at full subsidy, the unshielded APR is bounded; the floor is the
_V_MAXendpoint, equivalent to −50% APR. - Both sides scale with shielded share. A branch that's 5% shielded has tiny
uandv; a branch that's 80% shielded has much larger ones.
Constants
_S1 = 0.85e27 // shield share inflection
_ALPHA = 0.5e27 // curve steepness
_U_MULT = 2e27 // utilization multiplier
_V_MAX = 21979552668138406918 // full-shield unshielded cap (-50% APR)
_SHIELD_PRICING_CONSTANT_MIN = 50,000e18 // anti-grief floor on TVL
The shape of the curve: roughly linear at low shielded share, accelerating once shielded share crosses _S1. Shielded troves pay more per unit of shield protection in more-crowded branches.
Why this shape
If shield pricing were flat:
- A branch with 1% shielded supply would charge the same surcharge as one with 90% shielded supply, even though the second case offers far less protection (almost everyone is shielded; redemption walks the unshielded book for two seconds before hitting the shielded book).
- The unshielded book could be redeemed out faster than redemption demand actually creates, leaving everyone shielded.
The curve makes the surcharge expensive precisely when shielding is overcrowded, which is the right economic signal, and the matching unshielded discount on heavily-shielded branches makes staying unshielded attractive, which keeps a redeemable book available.
What the dashboard shows
A user reading the dashboard sees their effective APR as a branch rate plus one signed shield-share adjustment:
branch_APR = clamp(system_APR + branch_offset_APR, 0.25%, 50%)
your_APR = branch_APR + shield_adjustment
where shield_adjustment depends on the branch's shielded share:
shielded trove → +u (a surcharge; you pay extra)
unshielded trove → −v (a discount funded by the shielded surcharge)
Both u and v are non-negative and grow with shielded share. On a heavily-shielded branch, unshielded troves can borrow at a meaningfully lower APR than the branch rate, sometimes substantially less.
Why interest accrues continuously
Liquity v1 had a one-time issuance fee plus a decaying base rate. The continuously-accruing model is closer to a standard variable-rate loan and gives the controller a smoother knob to turn. There's no fee shock at borrow time.
The downside is that ICR moves over time even if collateral price is constant, interest accrues. A trove sitting at 110% with debt growing 5% APR will hit the MCR in a few months without any external event. Borrowers should plan for this.
How drip actually fires
Branches drip on three triggers:
- TroveManager.drip(): called from the Aggregator's drip-all sweep, or directly by any state-changing borrower op on the branch.
- InterestEngine.drip(): internal; called by TroveManager.
- Aggregator.dripAll(): sweeps every non-shutdown branch. Pays a keeper reward (capped at
DRIP_MAX_REWARD = 425 RD, scaled by elapsed time cubed up toDRIP_INTERVAL_2 = 8 hours).
The reward structure pulls some keeper attention without making the drip a profitable game by itself. Any user op already does the drip for free as part of its work.
Deep dive
- Peg & PI controller: where
system_ratecomes from. - Borrow rates (user view): same content, less detail.