How to interpret lending rates and calculate APYs for Compound, Aave and Euler contracts.
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:
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.
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.
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.
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.
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
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.