Computing Aave's borrow and deposit rates

Aave conveniently publishes a lot of raw data on their GraphQL database, such as the total liquidity, the total amount being borrowed, and borrow rate information. However, how does one compute the deposit rate? https://v2.aavewatch.com/ displays those rates but haven't shared their source code yet.

Borrow rate

In their first whitepaper, and their documentation, they describe the stable and variable borrow rates as a rather formula which depends on the reserve's current utilization. Utilization is the rate of money being borrowed vs the whole amount in the reserve.

The GraphQL database exposes all the constants necessary to compute a reserve's borrow rate.

They even expose the computed rate.

Finally the doc links to the code of a contract which computes that rate: https://github.com/aave/protocol-v2/...MathUtils.sol#L45.

Deposit rate

The formula is explained at https://docs.aave.com/risk/liquidity-risk/borrow-interest-rate#deposit-apy. I haven't looked at it in detail yet. It's always so hard to track down the meaning of each variable, since the same letter is sometimes used in different contexts with different meanings.

But while I was searching, I found all of this info being displayed on the official Aave webapp.

How did they compute Borrow and Deposit APY?

Luckily Aave shared their entire source code on GitHub. So I found their <MarketTable> component, which then led me to the compute Reserve data structure which contained the APY, which then led me to their math module, which computes that APY.

Digging into aave-js

Average rate

Here's how the data in <MarketTable> is fed.

calculateAverateRates computes:

valueToBigNumber(index1)
  .dividedBy(index0)
  .minus('1')
  .dividedBy(timestamp1 - timestamp0)
  .multipliedBy(SECONDS_PER_YEAR)
  .toString();

Here's the formula in human readable form:

\[\left({index1 \over index0} - 1\right) \times {SECONDS\_PER\_YEAR\ \over timestamp1 - timestamp0}\]

I have to admit, I don't understand the formula yet.

Let's take an example and assume that 30 days ago, the rate was 10% and today, it's 16%. Then the average rate would be:

\[ \left( {.16 \over .10} - 1 \right) \times {SECONDS\_PER\_YEAR\ \over { \left( today - 30\_days\_ ago \right) }_{in\_seconds}} \]

By computing date ranges in months instead, we get:

\[ \approx \left( 1.6 - 1 \right) \times {12 \over 1} \]

\[ = .6 \times 12 = 7.2 \]

That does not seem right.

Let's simplify our example. Let's assume we average between 0% today and 10% last year. Then it becomes obvious \(index0\) and \(index1\) should be based around 1, not 0. So an interest rate of 0% should be represented as 1.00 and 10% as 1.10:

\[ \left( {1.00 \over 1.10} - 1 \right) \times {SECONDS\_PER\_YEAR\ \over { \left( today - last\_year \right) }_{in\_seconds}} \]

\[ \approx \left( 0.909090909090909 - 1 \right) \times 1 \]

\[ = -0.090909090909091 \]

Should we interpret this as 9%? This example also shows another issue with the formula. How could the average rate end up being negative when both rates were over 0% to start with? Finally the formula isn't symmetrical, so whether the rate has been falling or increasing, the average will end up different, which is a bit counterintuitive. When I think of averages, I don't expect this kind of behavior.

I'll have to try the formula with more examples to get the hang of what it's supposed to represent. Right now, here's how I understand it:

\[ \left({index1 \over index0} - 1\right) \times {1 \over interval_{in\_years}} \]

If both rates are equal, the average is 0. If the end rate is larger than the start rate, then the average is positive. Otherwise it's negative.

If the end rate is larger than the start rate, then the average will grow as interval grows shorter.

Assuming the end rate is 30% and the start rate was 20%, and the interval is a year, we'd get:

\[ \left({1.3 \over 1.2} - 1\right) \times {1 \over 1} \]

\[ \approx \left({1.083333333333333} - 1\right) = 0.083333333333333 \]

\[ \approx 8.3\% \]

If the interval was half a year, the result would be double:

\[ \left({1.3 \over 1.2} - 1\right) \times {1 \over 0.5} \]

\[ \approx 16.7\% \]

Judging from this behavior, this indicator seems more like a "rate of growth" than an "average".

Another way to reason about this formula is to make it so that \(index1\) is the result of \(index0\). Let's try that.

\[ a = \left({index1 \over index0} - 1\right) \times {1 \over interval_{in\_years}} \]

\[ a \times interval_{in\_years} = \left({index1 \over index0} - 1\right) \]

\[ index1 = index0 \times \left( 1 + a \times interval_{in\_years} \right) \]

Now the formula makes a bit more sense. I guess you could call \(a\) the average relative growth rate per year. \(index0\) and \(index1\) are 1 based (eg 10% is represented as 1.1) while \(a\) is \(0\) based (eg .03 is a 3% growth). I wish they had documented the function as such.

APY

Btw, according to BankRate.com:

APY is calculated using this formula: \(APY= (1 + r/n ) ^ {n – 1} \), where \(r\) is the stated annual interest rate and \(n\) is the number of compounding periods each year. APY is also sometimes called the effective annual rate, or EAR.

So if an investment has a stated annual interest rate of 12%, and pays 4 times a year, we have r=0.12 and n=4, and APY is:

\[\left(1 + {0.12 \over 4} \right) ^ {4-1} = 1.03 ^ 3 = 1.092727\]

That's 9.3% APY.

The deposit APY is calculated here.

const supplyAPY = rayPow(
  valueToZDBigNumber(reserve.liquidityRate)
    .dividedBy(SECONDS_PER_YEAR)
    .plus(RAY),
  SECONDS_PER_YEAR
).minus(RAY);

Their formula is:

\[\left({liquidityRate \over SECONDS\_PER\_YEAR} + RAY\right) ^ {SECONDS\_PER\_YEAR} - RAY\]

with RAY being set to 10^27 (source) and SECONDS_PER_YEAR to valueToBigNumber('31536000') (source).

I'm not sure what they are trying to do with the RAY. Could it be that SECONDS_PER_YEAR is in ray too? Then the RAY they use has the same function as 1 in BankRate's API. If so, then the formula can be interpreted as:

\[\left(1 + {liquidityRate \over SECONDS\_PER\_YEAR}\right) ^ {SECONDS\_PER\_YEAR} - 1\]

It seems like they reimplemented the same APY formula as BankRate's with two differences:

  1. They raised the expression in parentheses to the power of \(n\) instead of \(n-1\). This should be fine since the difference between \(a ^ {31536000}\) and \(a ^ {31535999}\) should be negligible.
  2. They subtracted 1 at the end to get a clean percentage to display, eg \(.09 = 9\% \) instead of \(1.09\).