AAVE Source Code Analysis: The Proxy System

Summary
AAVE's proxy design is fairly complex. To understand it well, you have to peel it apart layer by layer.

Smart Contract Proxies

Upgradeable smart contracts rely heavily on proxy patterns, and AAVE’s proxy system is quite complex. To really understand it, you need to unpack it layer by layer.

Global AAVE proxy diagram:

AAVE-Proxy

According to OpenZeppelin’s proxy model, smart contract proxies are commonly divided into two kinds: transparent proxies and UUPS proxies.

Today, OpenZeppelin generally recommends the UUPS approach because it is more lightweight and easier to work with.

LendingPoolAddressesProvider

The first contract to understand is LendingPoolAddressesProvider, which sits at the center of the system. It acts both as a registry and as a management contract. It mainly serves two purposes:

  1. create, upgrade, and manage other contracts
  2. act as a registry of addresses so other contracts can look up one another through it

Several core contracts are created, upgraded, initialized, and managed through this contract:

  • LENDING_POOL
  • LENDING_POOL_CONFIGURATOR

Its name already gives away one of its main responsibilities: providing addresses. The following addresses are configured through LendingPoolAddressesProvider:

  • LENDING_POOL
  • LENDING_POOL_CONFIGURATOR
  • LENDING_POOL_COLLATERAL_MANAGER
  • POOL_ADMIN
  • EMERGENCY_ADMIN
  • PRICE_ORACLE
  • LENDING_RATE_ORACLE

How does LendingPoolAddressesProvider actually create and manage contracts? The rough flow is:

  1. When needed, it deploys a proxy template and sets the concrete implementation behind it.
  2. The core contracts listed above each store an addressesProvider variable. During initialization, they receive the provider address as a parameter.
  3. When those contracts need to call one another, they first query the other party’s address through addressesProvider, and then perform the call.

The code for creating or upgrading a proxy-backed contract looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  // Upgrade or create a contract
  function _updateImpl(bytes32 id, address newAddress) internal {
    address payable proxyAddress = payable(_addresses[id]);

    InitializableImmutableAdminUpgradeabilityProxy proxy =
      InitializableImmutableAdminUpgradeabilityProxy(proxyAddress);
    bytes memory params = abi.encodeWithSignature('initialize(address)', address(this));

    if (proxyAddress == address(0)) {
      // If the proxy does not exist, deploy it first and initialize it
      proxy = new InitializableImmutableAdminUpgradeabilityProxy(address(this));
      proxy.initialize(newAddress, params);
      _addresses[id] = address(proxy);
      emit ProxyCreated(id, address(proxy));
    } else {
      // If it already exists, upgrade it
      proxy.upgradeToAndCall(newAddress, params);
    }
  }

LendingPoolConfigurator

LendingPoolConfigurator, together with LendingPoolAddressesProvider, is used to create proxies for:

  • AToken
  • StableDebtToken
  • VariableDebtToken

The initialization code is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  function _initTokenWithProxy(address implementation, bytes memory initParams)
    internal
    returns (address)
  {
    // Deploy proxy contract
    InitializableImmutableAdminUpgradeabilityProxy proxy =
      new InitializableImmutableAdminUpgradeabilityProxy(address(this));

    // Point the proxy to the implementation and initialize it
    proxy.initialize(implementation, initParams);

    return address(proxy);
  }

Upgrading those token contracts is done like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  function _upgradeTokenImplementation(
    address proxyAddress,
    address implementation,
    bytes memory initParams
  ) internal {
    InitializableImmutableAdminUpgradeabilityProxy proxy =
      InitializableImmutableAdminUpgradeabilityProxy(payable(proxyAddress));

    proxy.upgradeToAndCall(implementation, initParams);
  }

Relationship diagram:

The broader takeaway is that AAVE layers its upgradeability. LendingPoolAddressesProvider manages the top-level core contracts, while LendingPoolConfigurator manages reserve-specific token proxies beneath that. That multi-level arrangement is one reason the proxy system feels more complicated than many simpler protocols.