Skip to content

Latest commit

 

History

History
160 lines (120 loc) · 14.5 KB

File metadata and controls

160 lines (120 loc) · 14.5 KB

Aave v3.5 features

Rounding improvements

Historically Aave has used half-up, or so called bankers rounding, for rounding operations. This has been a source of confusion and bugs in the past as it makes the rounding somewhat chaotic.

The aave protocol handles balances & yield by storing a scaledBalance and an index. The actual balance is then derived by multiplying the scaledBalance by the index or by dividing the amount by the index. The index has 1e27 precision, which means that the rounding error usually applies on the least significant digit of the asset. In practice that means that when e.g. depositing 1e18 of an asset, your balance might be 1e18 +-1. The rounding depends on time & index accrual, so it's difficult to exactly predict the outcome. The stata tokens and similar systems built on top therefore use a custom rounding function to ensure that the rounding is always in favor of the protocol (like it is done on the 4626 spec).

It's important to note that the rounding error is still technically unbounded. The index can grow almost(uint128.max) infinitely and the shares and the underlying share the same decimal precision. The intention of this change is to always round in favor of the protocol, to avoid insolvency situations.

This upgrade aligns the rounding methods by:

Rounding down when minting aTokens

When users supply(amount) to the protocol, the amount is rounded down. This means that from now on a supply will always be less than or equal to the amount the user supplied. A supply(n) will always yield m = n / index scaled balance.

Example: supply(1e18) would previously have resulted in an aToken balance equal to scaledAmount = halfUp(amount / index), so that amount' = halfUp(scaledAmount * index) = amount (+-precision loss). After the change the variance is limited to amount' = halfUp(scaledAmount * index) = amount (-precision loss).

Rounding up when burning aTokens

When users withdraw(amount) from the protocol, the amount is rounded up. This is done to ensure that the user always receives at least the amount they requested.

Example: withdraw(amount) would previously have resulted in releasing amount of underlying and burning scaledAmount = halfUp(amount / index) shares, so that amount' = halfUp(scaledAmount * index) = amount (+-precision loss).

Rounding down for aToken balance operations

balanceOf and totalSupply are now rounded down. Currently these methods sometimes overaccount by 1 wei, and sometimes underaccount by 1 wei. From now on they will no longer overaccount. This is done to ensure correct behavior in regards to user operations, so that e.g. withdraw(aToken.balanceOf(user)) will always redeem the full user balance.

Rounding up when minting vTokens

When users borrow(amount) from the protocol, the amount is rounded up. This is done to ensure that the protocol never underaccounts the user debt.

Rounding down when burning vTokens

When users repay(amount) or are being liquidated, the protocol must ensure that the repayment covers the full debt.

Rounding up for vToken balance operations

balanceOf and totalSupply are now rounded up. Currently these methods sometimes overaccount by 1 wei, and sometimes underaccount by 1 wei. From now on they will no longer underaccount. This is done to ensure correct behavior in regards to user operations, so that e.g. repay(vToken.balanceOf(user)) will always repay the full debt.

Transfers

On transfers, the scaled amount is rounded up. While for other methods the rational is obvious(and well specified on ERC4626), on transfers the "correct" path is more debatable. We think that currently the more "problematic" code-path is, if you pull x funds, but you get x-1. Therefore by rounding up the scaled amount, we ensure that the contract pulling receives at least the amount it requested.

Rounding for User Position Valuation in Base Currency

When calculating a user's health factor, the protocol must convert their asset and debt balances into a common base currency (e.g., USD). To ensure the protocol's safety, these conversions are rounded pessimistically.

  • Collateral (aToken) Value: The value of a user's supplied assets in the base currency is always rounded down. This ensures the protocol never overestimates the value of a user's collateral.
  • Debt (vToken) Value: The value of a user's borrowed assets in the base currency is always rounded up. This ensures the protocol never underestimates a user's debt. A key consequence of this is that any non-zero debt, no matter how small, will be valued as at least 1 wei in the base currency. This prevents situations where tiny "dust" debts could round down to zero after conversion, making them invisible to Health Factor calculations.

This approach of always rounding in favor of the protocol during health factor calculations is a critical security measure that protects it from potential insolvencies arising from rounding discrepancies. As a result of this change, the health factor of a user may be lower than in previous versions of the protocol.

Internal scaledAccounting

A lot of the precision loss is caused by having multiple conversions from unscaled to scaled and back. To name a few examples:

  • when validating caps, for the validation the scaledAmount * index + input.amount were considered to be the total supply - this is not accurate though as the conversion from input.amount to scaledAmount will always have a precision loss.
  • when minting tokens to the treasury, the protocol stores the scaledAmount in accruedToTreasury. When the protocol mints to the treasury, the accruedToTreasury is scaled up and then scaled down again, which can lead to precision loss.

While these problems are not critical, for the most part, they can be mitigated by consistently working with the scaled values throughout the protocol. Therefore, mint and burn now accept scaledAmount as an additional input, which allows avoiding repeated roundings. As a side effect, this also slightly reduces gas consumption across the board.

Improved flag logic

In v3.4 we increased the robustness of the flag logic for borrow operations, by switching validations from unscaled to scaled comparisons to checks against the actual balance (e.g. balanceZeroAfterBurn).

In v3.5 we decided to double down on these robustness improvements:

  • on repayWithAToken and withdraw, the collateral flag is now properly set to false when burning all aTokens in the process. In previous versions of the protocol, there were edge cases in which the collateral flag was not properly updated.

Improved allowance

Aave historically has never accurately tracked allowance. The reason for this is that in practice most operations are performed with the desired amount of assets, but the a/v token converting these amounts to shares. For allowance / approval, this means that the consumed allowance is not always equal to the amount transferred. While this problem is not perfectly solvable without breaking changes, in v3.5 the protocol ensures that the exact consumed allowance is burned if available.

Example: When a user calls transferFrom(sender, recipient, 100) in most cases the transfer will transfer slightly more than 100 tokens (e.g 101). This is due to precision loss between assets/shares conversion. In Aave versions < 3.5 this action would always result in burning 100 allowance. On Aave v3.5, the transfer will check the balance difference on the sender and discount up to the difference from the allowance. Example: The user transfers 100, but due to rounding he loses 102 balance. The allowance is reduced by up to 102. If the original allowance was only 100, the transaction will still pass for backwards compatibility.

Misc changes

  • smaller refactoring in LiquidationLogic making the code more consistent
  • repayWithAToken is only allowed when the user is still healthy after the repayment. This is done in order to prevent some edge cases where the user would have debt, but no collateral or now with the adjustments on rounding, bringing himself into liquidation area through selfRepayment.
  • eliminateReserveDeficit return value: The eliminateReserveDeficit function has been updated to return the uint256 amount of deficit that was actually covered. This returned value represents the lesser of the input amount and the actual deficit of the reserve at the time of the call. This provides clarity to the caller on the exact amount that was successfully written off.
    • Note: This is a non-breaking change. The migration from no return value to a uint256 return value is not expected to break any existing integrations.
  • In previous versions of the protocol, Mint and Burn events on AToken and VariableDebtToken did not always perfectly reflect the amount minted and burned due to imprecision in the calculation of the amountToMint and amountToBurn variables. In v3.5, the value emitted in Mint and Burn events now always accurately reflects the difference between the previous upscaled balance and the new upscaled balance. For AToken transfers, the Transfer event emits the input amount, while the BalanceTransfer event emits the precise scaled amount being transferred. Due to the new rounding logic, the actual change in unscaled balance might differ slightly from the input amount.
  • The control flow of borrow has been altered. While in previous versions of the protocol the borrow function would first check the hf limitations, from v3.5.0 the healthfactor check is performed at the end. Moving the check allows to de-duplicate the healthfactor related calculations and avoids issues due to non-equivalence in some edge cases.

Changelog

  • General changes:
    • Changed conversions between scaled and unscaled amounts:
      • In the AToken contract:
        • The balanceOf and totalSupply functions now round down (from scaled to unscaled).
        • The mint function now rounds down (from unscaled to scaled).
        • The burn function now rounds up (from unscaled to scaled).
        • Calculations of a transfer's scaled amount now round up in AToken (from unscaled to scaled).
      • In the VariableDebtToken contract:
        • The balanceOf and totalSupply functions now round up (from scaled to unscaled).
        • The mint function now rounds up (from unscaled to scaled).
        • The burn function now rounds down (from unscaled to scaled).
  • Pool contract:
    • The finalizeTransfer function now accepts scaled amounts instead of unscaled amounts. This avoids the precision loss caused by rounding the unscaled parameters.
    • The eliminateReserveDeficit function now returns the actual amount of deficit that was covered.
  • AToken contract:
    • The following functions now accept a scaledAmount parameter instead of the amount parameter, which avoids the precision loss caused by rounding the amount parameter:
      • mint
      • mintToTreasury
      • transferOnLiquidation
    • The burn function now accepts a new scaledAmount argument, which is the scaled amount of tokens to be burned. This avoids the precision loss caused by rounding the amount parameter.
    • Changed the logic and math of the allowance decrease in the transferFrom function.
  • VariableDebtToken contract:
    • The mint function now accepts a new scaledAmount argument, which is the scaled amount of tokens to be minted. This avoids the precision loss caused by rounding the amount parameter.
    • The burn function now accepts a scaledAmount parameter instead of the amount parameter, which avoids the precision loss caused by rounding the amount parameter.
    • Added new state variables __unusedGap and __DEPRECATED_AND_NEVER_TO_BE_REUSED.
    • Changed the logic and math of the borrow allowance decrease in the mint function.
  • DebtTokenBase contract:
    • Changed the logic and math of the borrow allowance decrease in the _decreaseBorrowAllowance function.
  • IncentivizedERC20 contract:
    • Added a new _spendAllowance function, which is used to decrease the allowance of a spender.
  • ScaledBalanceTokenBase contract:
    • The following functions now accept scaled amounts instead of unscaled amounts:
      • _mintScaled
      • _burnScaled
    • Changed the math of the amountToMint variable in the _mintScaled function.
    • Changed the math of the amountToBurn and amountToMint variables in the _burnScaled function.
  • Libraries:
    • BorrowLogic library:
      • The executeBorrow function now checks the health factor and the LTV of a user after the borrow operation by calling the new ValidationLogic.validateHFAndLtv function.
      • In the executeRepay function, the contract now checks the health factor of a user (user, not onBehalfOf) after the repay operation when the user is repaying with ATokens.
    • FlashLoanLogic library:
      • The executeFlashLoan function now rounds up the protocol fees for flash loans of type 0.
    • GenericLogic library:
      • In the calculateUserAccountData function, the precision in health factor calculations has been improved.
      • In the calculateAvailableBorrows function, the availableBorrowsInBaseCurrency variable is now rounded down.
      • In the _getUserDebtInBaseCurrency function, the amount in the base currency is now rounded up.
    • LiquidationLogic library:
      • The executeEliminateDeficit function now returns the actual amount of deficit that was covered.
      • In the executeLiquidationCall function, some rounding behavior has been changed:
        • The borrowerReserveDebtInBaseCurrency variable is now rounded up.
        • The isDebtMoreThanLeftoverThreshold variable is now rounded up.
      • In the _calculateAvailableCollateralToLiquidate function, the debtAmountNeeded variable is now rounded up when maxCollateralToLiquidate > borrowerCollateralBalance.
    • ReserveLogic library:
      • In the _accrueToTreasury function, the totalDebtAccrued variable is now rounded down.
    • SupplyLogic library:
      • The executeFinalizeTransfer function now accepts scaled amounts instead of unscaled amounts.
    • ValidationLogic library:
      • The following functions now accept scaled parameters instead of unscaled parameters, which avoids the precision loss caused by rounding unscaled parameters:
        • validateSupply
        • validateWithdraw
        • validateBorrow
        • validateRepay
      • The validateBorrow function no longer checks the user's health factor and LTV before the borrow operation.
    • A new TokenMath library has been added. It contains functions to perform conversions between scaled and unscaled amounts for A/V tokens.