Aave v3.4 is an upgrade to be applied on top of Aave 3.3. The focus on this release is on UX improvements and code simplification while at the same time preparing the protocol for an upcoming 3.5 that is already well in progress.
The Aave-GHO integration currently relies on various special cases, which make reasoning about the protocol harder than it should be. This special cases range from inconsistencies in balance handling, different management of debt accrual, the inability to flashloan to the discount model applied on top.
With the introduction of Umbrella and the GHODirectMinter it is time to deprecate the current implementation and to align it with the rest of the protocol.
aGHO
aGHO on the current Aave Core instance is a special implementation that has very little in common with other aTokens. Its main purpose is to trigger various hooks on actions.
These inconsistencies materialize in a different utilization model, the inability to flashloan GHO and other minor quirks.
vGHO
vGHO while working similar to a usual vToken also has some special mechanics which lead to complications and have been the source of various bugs. In contrast to usual vTokens the debt does not accrue as interest on the aToken (as aToken totalSupply is always 0), but instead accrues only on the vToken and is collected on repayment. This is done in order to support the discount model, but when analyzing onchain data, it became apparent that only a very small userbase ever made use of the GHO discount. Therefore, we think it is rational to drop it.
If a discount model is still desired, it could still be achieved on top via rewards. One option could be to use Merkle (e.g. via merit) if the goal is to only reward GHO borrowers. Another option could be to simply distribute GHO rewards to all stkAAVE users.
Therefore, what we propose is to align GHO on core with GHO on prime. In practice that means that GHO will work exactly as every other ERC20 listed on Aave, with the caveat that the supply is directly minted to the protocol. Opposed to GHO on prime:
- The IR should be static (as it currently is)
- The supply cap should be set, so that no user can supply GHO
With this alignment, various complexities and special cases from the protocol are permanently removed:
- All reserves will have virtual accounting enabled
- Burning bad debt no longer relies on special logic that discounts protocol fee in some special cases
- GHO flashloans are enabled, eliminating the need for custom adapters, and in most cases making the flash-mint obsolete
- Debt now directly accrues to the treasury as
aTokens, not only on repayment improving the treasury accounting balanceOf(user) == scaledBalanceOf(user) / normalizedDebt(like on every other reserve)- Umbrella does no longer need special handling for GHO
This alignment comes with a huge reduction in code complexity, and gas consumption, paving the way for faster and less complicated iteration on the protocol & integrations.
Being able to batch multiple transactions has been a frequently requested feature on the aave protocol. This functionality can greatly improve ux as it allows users to e.g. supply & enable an eMode & borrow in a single transaction. In addition to that being able to permit a separate entity to execute transactions on my behalf opens new use-cases & the possibility of transaction subsidies. Therefore Aave v3.4 ships with multicall support.
When talking to various integrators after the release of Aave v3.2 it became apparent that one important ux feature that is currently missing is the ability for contracts to:
- enable/disable a collateral
- enter/switch an eMode
on behalf of a user. Therefore Aave 3.4 introduces the concept of "positionManagers" - addresses that can perform setUserUseReserveAsCollateralOnBehalfOf & setUserEModeOnBehalfOf once given approval by a user.
The user can give & revoke an approval by calling approvePositionManager(address manager, bool approve). A position manager can renounce the role by calling renouncePositionManagerRole(address user). All methods are noops in case the current state is already the desired state.
Unbacked supply was part of a feature called Portals on the original aave-v3 release post.
Eventually the feature was never used, but occupies an important amount of contract size while also costing gas on most transactions.
Therefore, in this version of the protocol we decided to drop the feature to make room for other improvements.
The feature might be added back in a future iteration if a use-case emerges and/or code-size becomes less of a concern(e.g. via Fusaka/eof).
While all reserves already share the same interestRateStrategy, in Aave v3.3 the strategy was defined per reserve which led to one unnecessary sload per touched reserve(s).
Therefore, in Aave v3.4 one immutable variable RESERVE_INTEREST_RATE_STRATEGY was added to the Pool contract. It will be used as an IR address for each reserve in Aave v3.4.
This variable will be set in the constructor of the implementation contract.
While all reserves already share the same incentivesController, in Aave v3.3 the incentivesController was defined in a state per token, which leads to unnecessary sloads per touched reserve(s).
Therefore, in Aave v3.4 one immutable variable REWARDS_CONTROLLER was introduced on the IncentivizedERC20 base class.
This variable will be set in the constructor of the implementation contract.
A new immutable variable POOL was added at the AaveProtocolDataProvider contract. All external calls to ADDRESSES_PROVIDER.getPool() were replaced with an access to the immutable variable.
This results in meaningful gas savings across the board.
Currently, the treasury on each aToken is stored in storage, although it is the same for each aToken - even across pools.
This causes unnecessary storage reads on liquidations and mintToTreasury.
Therefore, in Aave 3.4 the treasury was moved to an immutable, which reduces gas cost on these methods.
Aave has historically used error codes opposed to Error signatures, because there was a preference for require(cond, error) which did not support signatures.
As on Aave v3.4 the codebase was upgraded to solc 8.27, require(cond, Error()) is now supported.
This greatly improves UX, as explorers and simulations now show helpful errors opposed to cryptic numeric Error codes.
At the same time, the change results in minor gas & codesize savings across the board.
While this change could be breaking for anyone relying on exact error codes, it is important to note that:
- Every single Aave release since v3.0 had breaking changes in regards to error emission (either due to new Errors or the order of Errors)
- We checked all the major integrations and did not find a single example of people relying on exact error codes
Currently there are 3 different versions of the aToken deployed:
- the main one you can find on this repository
- a custom version for UNI token voting delegation
- a custom one for aAAVE that can be found here
In the previous version the implementation of UNI aToken has a function delegateUnderlyingTo for the Pool admin that allows to delegate voting power of aToken suppliers to some delegatee. Delegating the suppliers UNI token to the AAVE DAO or similar is debatable and the feature has never been used. Therefore in Aave 3.4 the implementation will be changed to a default one and the function will be removed.
In order to remove code complexity between aAAVE and other aTokens, the storage layout between the versions was aligned.
In practice this means that on ScaledBalanceTokenBase the .balance storage was changed from 128 to 120 bits. For Aave this storage change is perfectly fine given the following rational:
- the protocol works with
uint256and has no assumptions about the token storage already - the
uint120storage was audited for the case of AAVE, but the same artificial limitation can be applied for all tokens given that2^120 ~= 10^36, still accepts values that exceed what could ever be required - the "freed"
8bits are at the end of the current balance and always0(except for aAAve where the occupy delegation related storage) - the storage is not directly exposed on the token (no interface change)
The implementation for aAAVE was upgraded in line with the other tokens: ATokenWithDelegation diff, BaseDelegation diff.
- Gas usage of
executeUseReserveAsCollateralwas greatly reduced by optimizing storage access - Gas usage improved by shuffling
virtualUnderlyingBalanceinto the position of pre 3.4unbacked - Minor codestyle improvements on
executeRepay - Usage of imported events from the interface contracts instead of redeclarations
- Skip unnecessary calculations on a subset of transfers
- Self-liquidation is now forbidden. While this is a breaking change, it's unlikely to affect anyone, as there are essentially no onchain traces of people relying on this functionality.
SafeCastwas upgraded from openzeppelin v4 to v5. The main difference is the usage of error signatures, reducing the codesize of various contracts.VersionedInitializablenow bricks the initializer on the implementation, so implementations no longer have to be initialized in order to prevent malicious initialization.- Now all fees from flash-loans are sent to the
RESERVE_TREASURY_ADDRESSin the form of the underlying token. Also, the functionFLASHLOAN_PREMIUM_TO_PROTOCOLin thePoolcontract now always returns100_00value. - Improved the accuracy and gas consumption of the
calculateCompoundedInterestfunction without changing the formula. Inside calculations of thesecond_termandthird_termvariables now at first the function performs multiplications byexpand then divides bySECONDS_PER_YEAR. Previously it was the other way around, first there was division, then multiplication. - Replaced the use of the
ecrecoverfunction call with the OpenZeppelin'sECDSA.recoverfunction call.
Solc was upgraded from 8.20 to 8.27
Poolcontract:- Now has the second argument
IReserveInterestRateStrategy interestRateStrategyin the constructor. It is a default IR contract that will be used for each reserve in the system. - Function
getReserveDatanow doesn't read theinterestRateStrategyAddressfield from theReserveDatastructure from the storage, now it reads theRESERVE_INTEREST_RATE_STRATEGYimmutable variable. - Function
initReservethat is called by thePoolConfiguratorcontract in the process of a new reserve token initialization:- Removed input argument
address interestRateStrategyAddressbecause each reserve has the same IR contract
- Removed input argument
- Removed the setter function
setReserveInterestRateStrategyAddress. There is no need in this function from now on. - Removed
mintUnbacked,backUnbackedandgetBridgeLogic - Implements openzeppelin
Multicall, ERC2771Context - The function
FLASHLOAN_PREMIUM_TO_PROTOCOLnow always returns100_00value. - The function
updateFlashloanPremiumsis renamed to theupdateFlashloanPremiumfunction and now accepts only one argument - only total flash-loan premium.
- Now has the second argument
PoolInstancecontract:- Changed the
POOL_REVISIONpublic constant value from6to7.
- Changed the
AaveProtocolDataProvidercontract:- Made some minor gas optimizations in the
getInterestRateStrategyAddressfunction. getIsVirtualAccActiveis deprecated and always returns true.- Added a new immutable variable
POOLinto the contract and in theIPoolDataProviderinterface too
- Made some minor gas optimizations in the
ReserveLogiclibrary:- In the function
updateInterestRatesAndVirtualBalancethe variableinterestRateStrategyAddressis now read from theRESERVE_INTEREST_RATE_STRATEGYimmutable variable instead of the storage.
- In the function
ConfiguratorLogiclibrary:- Function
executeInitReservethat is called inside of thePoolConfiguratorcontract in the process of a new reserve token initialization:- The variable
interestRateStrategyAddressnow is taken from return values of theinitReservefunction in thePoolcontract. Previously it was taken from theInitReserveInputstructure that was passed by a caller, but now this structure doesn't have this field (this structure was changed too).
- The variable
- Function
PoolConfiguratorcontract:- Function
setReserveInterestRateStrategyAddresswas deleted because now every reserve in the system has the same IR contract. - Function
initReservesthat accepts an array ofInitReserveInputstructs, no longer has ainterestRateStrategyAddressfield. - Removed
setUnbackedMintCapas unbacked is no longer a thing.
- Function
ReserveDataLegacyandReserveDatastructures:- Field
interestRateStrategyAddressis new deprecated.
- Field
InitReserveInputstructure:- Removed the
interestRateStrategyAddressfield because each reserve in the system has the same IR contract.
- Removed the
AToken&VariableDebtToken:- The
IncentivizedERC20token base now exposes a newREWARDS_CONTROLLER, while also maintaininggetIncentivesControllerfor backwards compatibility. setIncentivesControllerwas removed as the incentives controller is no longer mutable,- The initialize no longer accepts a
incentivesController, but the constructor now accepts arewardsController handleRepaymentwas removed as it was only required for GHO and is now a noop- The
aTokenreceives a publicTREASURYimmutable - The initialize no longer accepts a
treasury
- The
LiquidationLogiccontract:- rename "user" was renamed to "borrower", to increase the precision on the wording.
- [BREAKING]: self-liquidation is no longer allowed
PoolLogiccontract:- The
PoolLogiccontract was extended with two methodsexecuteSyncIndexesState&executeSyncRatesState, which simply replicate what was previously on the Pool itself. This is done to free some code-space on the pool itself.
- The
FlashLoanLogiclibrary:- Now all fees from flash-loans are accrued to the
RESERVE_TREASURY_ADDRESS.
- Now all fees from flash-loans are accrued to the
MathUtilslibrary:- The
calculateCompoundedInterestfunction was improved to be more accurate and gas efficient without changing the formula.
- The
For users of the protocol, no migration is needed. As the protocol upgrade path is slightly more complicated than in previous upgrades, this section describes the upgrade path for various of the introduced features.
As the current aGHO acts as both the aToken and the GHO facilitator, the migration can be perceived as two-step process: 1) the migration of the facilitator 2) the alignment of behavior.
In practice, the following steps will be performed on the proposal:
- Calling
aToken.distributeFeesToTreasury()to distribute pending fees to the treasury. - The GHO
aTokeninstance is updated with a custom implementation that offers aresolveFacilitator(uint256 amount)function that allows burningGHO. - A new
GHODirectMinteris registered as a facilitator(NF) with the samecapacityas the existingaToken facilitator(AF). - The NF, does mint the current
aToken facilitator leveland supply it asaTokenson the Pool. resolveFacilitatoris called, burninglevelamount ofGHO- With the GHO being burned, the
levelof AF is reduced to0, so it can now be safely removed viaGHO.removeFacilitator - The
pool,configurator,aTokenandvTokenare now being updated to v3.4, which will result invirtualAccountingbeing enabled for GHO. - Now the
reserveFactoris increased from0%to100%and thesupplyCapis set to1 vGHOis upgraded to align with the default implementation + anupdateDiscountDistributionnoop.
Note: The cap limitation is in place to prevent users from accidentally supplying GHO(as it is no collateral and there is a 100% reserve factor, it would never be intentional).
Note: The GHO vToken, while being 100% compatible with the default implementation, will contain an updateDiscountDistribution noop so that there are no issues with the stkAAVE transfer hook.
Note: The pending discount for users will be lost on the upgrade. Therefore we recommend for a DAO related service (e.g. Dolce Vita) to iterate all discounted GHO borrowers and repay 1 wei of GHO on behalf to apply the discount one last time, slightly before proposal execution.
Note: Umbrella can be simplified as there no longer is a special path for coverage with assets that don't have virtualAccounting enabled.
The PoolConfigurator contract should emit the FlashloanPremiumToProtocolUpdated event in the migration. This event was deprecated, now the FLASHLOAN_PREMIUM_TO_PROTOCOL function always returns 100_00 value. The emit should contain these values:
oldFlashloanPremiumToProtocol: the old value of theFLASHLOAN_PREMIUM_TO_PROTOCOLfunction.newFlashloanPremiumToProtocol: value100_00.
As virtual accounting is now always active, fetching getIsVirtualAccActive is becoming obsolete.
That said, upon investigating existing contracts we noticed that there are various instances of people hard-coding the PoolDataProvider or directly decoding configuration.getIsVirtualAccActive.
To maintain compatibility with these contracts, the boolean flag will be carried in the configuration for the upcoming version(s).
As the storage slot of virtualUnderlyingBalance was moved, the Pool initializer will copy the value from the deprecated __deprecatedVirtualUnderlyingBalance slot to the new virtualUnderlyingBalance slot.
There are no user-facing changes related to this change.
In addition to the points mentioned above, the upgrade should upgrade
- all logic libraries as the InterestRate is now passed as a parameter instead of being fetched from storage.
- all tokens, due to changes from storage to immutables
- the pool configurator, to account for the changed signatures
- the config engine, as the signature of token initialization changed
The GHO FlashMinter is not affected by the upgrade.
That said, for contracts that rely on flashloans, GHO no longer needs to be handled as a special case - GHO can be flashloaned as any other asset. This makes contracts like the custom GHO Debt Swap obsolete.
When currently supplying(via transfer of supply) aToken to a user they might enable as collateral.
There are certain scenarios in which they don't. For a future version 3.5 we are considering changing this behavior, therefore we recommend migrating to the more explicit positionManager/multicall approach.
Instead of assuming that an asset will be enabled as collateral, for interactions on your own behalf, do multiCall([supply, enableAsCollateral]) for interactions on behalf of another entity, use the position manager functionality (approvePositionManager + supply, enableAsCollateralOnBehalf).