Paboxo

Interest accrual and reserves

How interest is booked, how it is split between lenders and reserves, and what "paid in full" means.

One accrual

Each time accrueInterest() runs, it charges the whole window since the last booking at the current rate:

Δt=tnow−tlast,i=⌊B⋅r⋅Δt1018⋅Y⌋\Delta t = t_{\text{now}} - t_{\text{last}}, \qquad i = \left\lfloor \frac{B \cdot r \cdot \Delta t}{10^{18} \cdot Y} \right\rfloor

Where:

  • BB — totalBorrowAssets before the booking, USDG base units.
  • rr — the borrow rate from the curve, at the moment of booking, applied to the whole window.
  • Δt\Delta t — seconds since lastAccrued.
  • YY — 31,536,000 seconds.

One multiplication chain, one division, so it rounds once. Source: InterestRateModel · 0x8390…D741.

The split

ires=⌊i⋅Rf1018⌋,isup=i−ires,Rf=reserveFactor(router)i_{\text{res}} = \left\lfloor \frac{i \cdot R_f}{10^{18}} \right\rfloor, \qquad i_{\text{sup}} = i - i_{\text{res}}, \qquad R_f = \texttt{reserveFactor(router)}

Where RfR_f is the reserve factor in effect: 10% whenever nothing has been stored, which is the case on every mainnet market, and a real 0% when 0 has been set explicitly (see effective reserve factor). The reserve share rounds down and lenders take the remainder, so ires+isup=ii_{\text{res}} + i_{\text{sup}} = i exactly — no unit is charged to a borrower and credited to nobody. Source: InterestRateModel · 0x8390…D741, InterestRateModel · 0x8390…D741.

Booking

B+=i,S+=isup,R+=ires,tlast←tnowB \mathrel{+}= i, \qquad S \mathrel{+}= i_{\text{sup}}, \qquad R \mathrel{+}= i_{\text{res}}, \qquad t_{\text{last}} \leftarrow t_{\text{now}}

Source: LendingPoolRouter. Three guards come first:

CaseWhat happens
Δt=0\Delta t = 0Nothing
B=0B = 0lastAccrued moves forward, nothing is charged — a window with no debt is not billed to the next borrower
i=0i = 0Returns without moving lastAccrued, so the window stays open and is charged in full later

A borrow closes the window regardless: it accrues, then sets lastAccrued to now before the new debt is added. Without that, a market whose only debt is dust could keep a window open indefinitely under the third guard, and the next borrower would be charged the whole carried period on fresh debt. What a borrow drops is the floored remainder, under one base unit.

The third guard is what stops interest from being truncated away by frequent calls. There is no stored remainder, though: each booking still drops the fraction of a base unit below ii. So "paid in full" means to within less than one base unit (0.000001 USDG) per booking.

The shortest window that books anything is

Δt∗=1018 YB r\Delta t^{*} = \frac{10^{18}\,Y}{B\,r}

which is 0.07 s in the illustrative market: there, every accrual books something. It grows as debt and the rate shrink.

Who triggers it

accrueInterest() is public, permissionless and not pausable: anyone can call it. It also runs inside borrow, repay, liquidation, supplying and withdrawing collateral, the vault's supply and withdrawal of liquidity, and every Earn vault deposit, mint, withdraw and redeem, which accrue every market first.

Every market prices at its vault's utilisation, which the vault stores and moves only at a checkpoint, after every market has accrued at the old value. The pools call the vault's permissionless accrueInterestAll() (accrue every market, then checkpoint) before and after every borrow, repay and liquidation; the vault's own deposit, withdraw and allocation calls accrue every market at their start and checkpoint at their end. accrueInterestAll() is not paused with the vault, so a paused vault never blocks a repay or a liquidation. Between two checkpoints every market's rate is constant, so the order in which markets accrue does not change what any of them is charged. Between bookings, interest is owed but not yet written to storage; views such as totalBorrowAssets show the last booked figure.

Worked example

On mainnet at block 93419456 (timestamp 1792496007) only WETH/USDG had debt: BB = 50000840, last booked at 1792495666, 341 seconds earlier, at rr = 7119808825931708:

i=⌊50000840×7119808825931708×3411018×31536000⌋=3i = \left\lfloor \frac{50000840 \times 7119808825931708 \times 341}{10^{18} \times 31536000} \right\rfloor = 3

3 base units, all to supply (⌊3×0.1⌋=0\lfloor 3 \times 0.1 \rfloor = 0 to reserves): calculateInterest(router, 341) returned (3, 3, 0), and the vault's pendingInterest(pool) 3. NVDA/USDG and SPY/USDG had no debt, so their accruals only move lastAccrued forward (the B=0B = 0 guard above). Amounts that small hide the rounding, so the example uses the illustrative market, last booked 7 days earlier:

Δt=604800 s\Delta t = 604800 \text{ s}

With B=B = 20010000000 and r=r = 23759375000000000 (from the curve):

i=⌊20010000000×23759375000000000×6048001018×31536000⌋=9117741i = \left\lfloor \frac{20010000000 \times 23759375000000000 \times 604800}{10^{18} \times 31536000} \right\rfloor = 9117741
ii pendingreserve iresi_{\text{res}}to supply isupi_{\text{sup}}
Illustrative NVDA/USDG9.1177410.9117748.205967

All in USDG. For a year-long window the same formula gives 475425093 / 427882584 / 47542509 — 475.425093 USDG of interest, 427.882584 to supply and 47.542509 to reserves. That is what calculateInterest(router, 31536000) would return in that state.

Compounding

Because each booking grows BB, the next booking charges interest on interest. Compounding happens at the frequency of accruals, not continuously and not on a schedule. The effect on the annual figure is on APR and APY.

Reserves

Reserves sit in the market as cash, and they count toward its solvency: a withdrawal of liquidity and a borrow are both allowed only while

S+R≥BS + R \ge B

Source: LendingPoolRouter, LendingPoolRouter. The owner can take reserves out only while that still holds afterwards (LendingPoolRouter). If bad debt ever exceeds a market's supply, the excess is charged to reserves — see bad debt.

On this page