Calculating interest rates

How to interpret lending rates and calculate APYs for Compound, Aave and Euler contracts.

A guide to calculating on-chain interest rates - Ration. 07.11.2022.

Two approaches to calculating lending rates

The typical technique for calculating on-chain lending rates is to take the rates quoted by the contracts and attempt to translate these to a standardised APR or APY. To do that with precision you need to know exactly how interest accumulates inside the contract, whether it accrues per block or compounds every second or, in the case of Compound, only compounds when the market is touched (i.e. lend, borrow, repay or withdraw). If you're calculating historical interest rates then you'll also want some way of reflecting the inter-day fluctuations and accounting for additional interest that's not tracked by the lending rates at all like Aave flash loan fees which are awarded to depositors. This trickiness explains why every analytics dashboard is showing slightly different APYs for the same lending pools.

An alternative technique for generating an APR/APY is by measuring the actual interest that has accumulated inside the contract over time and extrapolating a rate from the daily change. Interest is tracked in Compound, Aave and Euler contracts by the index (or accumulator) which acts as the source of truth about historical interest in the market such that the change in the index between two dates gives us the exact amount of interest accrued in that period in relative terms, taking into account the lending rates and their accrual rate, any additional sources of interest and any compounding effect. By using the index to calculate historical interest rates we also average out the inter-day peaks and troughs that you might get skewed by if you’re just taking daily snapshots of the lending rates.

Here's how the two approaches play out for Compound V2 USDC:

Three different APR calculations for cUSDC that I've seen out in the wild with two using borrowRatePerBlock and different assumptions about daily block rates (5760 blocks per day and 6500 blocks per day) and another using the daily change of borrowIndex. Notice how the daily change in borrowIndex jumps post merge reflecting the increased block rate.

Two approaches then:

1. apr = (nativeRate * nativeAccrualPeriodsPerYear) + expectedAdditionalInterest 2. apr = (currentDayIndex - previousDayIndex) * 365

The rest of the article will describe how to interpret the native lending rates, i.e. the rates you can find by querying the contract or scanning the contract logs, for the three major lending pool protocols.

How to interpret Compound rates

In Compound V1 and V2 the interest accrues every block but is only applied and compounds when the market is touched (i.e. lend, borrow, repay or withdraw) which could be every block in busy periods or once every few days for the quieter assets. Looking into the contract you can see that between market touches there’s a linear interest calculation applied.

Of course you could go to the effort of calculating compound interest here at the contract level to guarantee something like continuous compounding, and Euler does that, but that brings some additional gas cost and, as shown by the table below, beyond a certain frequency the compounding effect doesn’t make all that much difference to the APY. Trade offs basically.

compound frequency makes little difference to the APY beyond once a day
APR (%) APY (1 compound/day) APY (86400 compounds/day)
1 1.00500287236815 1.00501665618635
5 5.12674964674473 5.12710936245873
150 346.793452386716 348.168890910734

To generate an APY for Compound we’ll need to take the per-block rate and make an assumption about the number of blocks in the year (not trivial pre-merge) and another assumption about how many compounding periods there are.

blocksPerYear = 7200 * 365 apr = (borrowRatePerBlock / 10^18) * blocksPerYear periods = 365 // assume interest compounds once a day apy = Math.pow((apr / periods) + 1, periods - 1) - 1

The table below shows how much difference the block frequency can make to the APR and most analytics providers have made different assumptions here.

block frequency makes a big difference to the APR
borrowRatePerBlock APR (5000 blocks/day) APR (6000 blocks/day) APR (7200 blocks/day)
10000000000 1.825 2.19 2.628
50000000000 9.125 10.95 13.14

In Compound V3 the lending rate is quoted per-second with similar compounding behaviour as V2.

How to interpret Aave rates

In Aave V2 and V3 the interest rate is tracked as an APR that accrues every second which makes the accumulation of interest more predictable than Compound V2's per-block rate. Deposit interest compounds only when the market is touched while borrow interest compounds every second due to an additional calculation performed in the contract (who knows why supply and borrow interest are handled differently here?).

To calculate an APY we’ll need to use the APR quoted by the contract and anticipate the frequency of market touches (for deposit interest at least) and also take into account an additional bonus for deposit interest generated by flash loan fees.

depositApr = (liquidityRate / 10^27) + flashLoanFeeBonus depositApy = Math.pow((apr / periods) + 1, periods - 1) - 1

How to interpret Euler rates

Out of the three major lending contracts Euler has the most predictable flow of interest since the internal lending rates are quoted per-second and the compounding effect is guaranteed to be every second for both depositors and borrowers. To calculate an APY here we can just take the per-second rate and multiply by the number of seconds in the year to get an APR and then apply a typical APR to APY conversion.

apr = (interestRate / 10^27) * secondsInYear apy = Math.pow((apr / secondsInYear) + 1, secondsInYear - 1) - 1

Thanks to feedback from @samlafer and an enlightening twitter thread between @samlafer and @euler_mab and various messages between stopher and Robert L. in the Compound discord.

Join our discord to chat.