aave / aave-v3-core

This repository contains the core smart contracts of the Aave V3 protocol.

Home Page:https://aave.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Difference between deposit interest and borrow interest calculation rule.

nzhl opened this issue · comments

The code shows that when calculating the liquidity index calculateLinearInterest is used but in the case of borrow calculateCompoundedInterest is used. After a little googling, i found the closed issue #702

image

But i am still quite comfused, since "the interest is linear in the supply side and compounded in the debt side", where did the extra compounding interest part go?

I tried to find the answer from aave doc but failed.

@nzhl There is no extra compounding interest. Interest get computed based on rates and liquidity rate is calculated from the borrow rate. You can check the implementation at DefaultReserveInterestRateStrategy.

Yes, it's true liquidity rate is calculated from the borrow rate. But when calculating the borrow index based on borrow rate, compounding is involved but that's not the case for liquidity index. (notice the calculateLinearInterest and calculateCompoundInterest )

They should calculated in the same rule IMO. Did I miss something?

They should calculated in the same rule IMO. Did I miss something?

Why? The linear and interest compounding is used just as a way to accumulate the interest. The accumulation depends on the rates, and one rate is based on the other so there is no excess or diff between them.

Considering the following case:

  1. Init a new lending pool,
  2. User A supply 100 ETH
  3. User B borrow 50 ETH
  4. let's say borrow rate is 30%, reserve factor is 5%. Since usage ratio is 50%, we have liqudity rate 30% * 50% * (1-5%)= 14.25% right ?
  5. a year later after B's borrow, borrow index should be (1.3 / (365 * 24 * 3600) + 1) ** (365 * 24 * 3600) = 3.669 (calculateCompoundInterest),
  6. so B should repay 50 * 3.669 = 183.45, in which 133.45 is the interest accumulated
  7. for A, liquidity index = 1.1425, A can receive 100 * 1.1425 = 114.25 (calculateLinearInterest), in which 14.25 is the intereset accumulated
  8. for the pool, reserve factor is 5% percent, accumulated interest should be 100 * 30% * 50% * 5% = 0.75
  9. Now B repay all and then A withdraw all, 133.45 - 14.25 - 0.75 = 118.45 left in the pool which does not belong to anyone.

I am sorry maybe i misunderstood the code or ignored some significant details. The above is basically what i am struggling about.

@nzhl some design/architectural tips that maybe will help you to visualize the system:

  • Both supply and borrow indexes are actually compounded inter-actions (check here https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/logic/ReserveLogic.sol#L301), but one is linear (supply) and the other compounded (borrow) intra-action.
    Meaning that between let's say 2 borrow() user actions, both indexes will compound themselves, but inside each action, the accumulation based on time since the last update and currently stored rate (at the beginning of the action here https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/logic/ReserveLogic.sol#L97) will be linear vs compounded.
  • The rationale of the previous is having a slight "overcounting" on the borrowing side, to never have due to precision any potential problem of insolvency. When the frequency of actions is regular in the pool, the difference is insignificant.
  • You can visualize rates and indexes as completely independent components, but consistent between themselves because of some properties:
    1. Borrow rate (considering variable and stable) is always higher or equal to the supply rate.
    2. The "growth" of indexes from the rates (as there can be different income flows for the supply index) follows that supply index growth will always be lower or equal to the growth of the borrowing index.
    3. Reserve Factor has no connection with indexes. In practice, the system "connects" them via the dynamics here https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/logic/ReserveLogic.sol#L236 and in the rate strategy (here https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol#L223). What happens going more in details is that 1) on rate strategy the reserve factor gets discounted from the liquidity rate (not touching the borrowing rate) which high-level means "discount this % portion of borrow interest from the claims of all the depositors" 2) this "discount" becomes actually aToken balance calculated from the total debt accumulated here https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/logic/ReserveLogic.sol#L274. This last point is the core of everything because the "growth" aToken for all depositors comes from the supply index, but for the collector/treasury, the growth actually comes from that minting action calculated from the total debt on the previous link. Important to highlight that this is a "virtual" mint on Aave v3, as it goes to a variable reserve.accruedToTreasury instead of aToken balance, but they are factually the same.
    4. Whenever the "mint" to reserve.accruedToTreasury happens, the collector/treasury is de-facto just another depositor and this is transparent for the system of rates and indexes: borrow rate keeps growing (covering all claims of depositors + treasury/collector via reserve.accruedToTreasury coming from rates), supply rate keeps growing but discounting the RF, indexes keep compounding inter-action and linear vs compounded intra-action.

So to summarize, apart from the diff caused by intra-action interest dynamics (really small) the claims of aToken holders (+ accruedToTreasury) multiplied by the supply index should be equivalent to the total debt multiplied by the borrowing index. Potentially even a bit higher, whenever the supply index receives extra income flows for example from flash loan fees.
If you would like to see better with numbers, I recommend you create a test on the codebase with some sample users, amounts, and time passing, and see how the system reacts

I think the "intra-action interest" is what i am seeking for. The more ppl interact with the contracts with less diff there will be between borrow and supply. Big thx to @eboadom @miguelmtzinf , you guys are very nice 🚀

commented

@nzhl Nice for this issue

Hi everybody, I have a question regarding

Both supply and borrow indexes are actually compounded inter-actions (check here https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/logic/ReserveLogic.sol#L301), but one is linear (supply) and the other compounded (borrow) intra-action.
#819 (comment)

Just to clarify, does that mean that both the supply and the borrow liquidity indexes are updated using compounded interests "inter actions" ?

With "actions" I am assuming you mean an action that changes the rate and therefore require a liquidity index update so supply, borrow, withdraw and repay

I am asking because I looked into the code and could not find where this happens: I see the supply liquidity index always updated with linear interests and the borrow liquidity index always updated with compounded interests

Let me share my analysis so you can double-check it

So the Pool.borrow() is defined here

It calls BorrowLogic.executeBorrow() here

which internally calls reserve.updateState(reserveCache); here

reserve.updateState(reserveCache);

It is defined in ReserveLogic.updateState() here

internally it calls _updateIndexes(reserve, reserveCache); here
https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/logic/ReserveLogic.sol#L103

It is defined in ReserveLogic._updateIndexes() here

This function updates 2 liquidity indexes for the reserve of a specific token

  • the reserve.liquidityIndex that should represent all the accrued interests so far and the implementation is here

if (reserveCache.currLiquidityRate != 0) {
uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(
reserveCache.currLiquidityRate,
reserveCache.reserveLastUpdateTimestamp
);
reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul(
reserveCache.currLiquidityIndex
);
reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128();
}

the reserve.variableBorrowIndex that represents the variable borrow rate, representing the accrued interests the borrower has to pay and the implementation is here

// Variable borrow index only gets updated if there is any variable debt.
// reserveCache.currVariableBorrowRate != 0 is not a correct validation,
// because a positive base variable rate can be stored on
// reserveCache.currVariableBorrowRate, but the index should not increase
if (reserveCache.currScaledVariableDebt != 0) {
uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(
reserveCache.currVariableBorrowRate,
reserveCache.reserveLastUpdateTimestamp
);
reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(
reserveCache.currVariableBorrowIndex
);
reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128();
}
}

so as you can see the first index is updated with linear interests while the second index is updated with compound interests which seems quite weird since the documentation says the linear update should not be written in storage as they should both compound

The view methods seem aligned with the doc since the getNormalizedIncome() here

returns the supply side income and the getNormalizedDebt() here

returns the debt side amount using compound interests