Formal Verification of Aave v3: A Review

In this case study we look at how the Certora formal verification Prover can be used for discovering high-severity vulnerabilities in the Aave lending protocol.

Introduction

In this case study we look at how the Certora formal verification Prover can be used for discovering high-severity vulnerabilities in the Aave lending protocol. Certora uses symbolic execution to evaluate all possible inputs to functions being tested, allowing us to uncover vulnerabilities that typical unit tests covering only specific cases can't. This was the case for Aave when it underwent this assessment by Certora, their test suite consisted of multiple hardhat tests with specific values for assessing specific system behavior. Expected behavior however can often bias test creation and leave out potential edge cases that may lead to unexpected functionality and consequently security vulnerabilities as we will see with the vulnerability discussed below.

The Contracts

For the review conducted by Certora 6 contracts were included in scope, but we'll only be focusing on the Pool contract since this is where the vulnerability we'll be looking at exists:

  • Pool - the main entry point into the Aave Protocol, most user interactions (borrowing, lending, etc.) with the Aave Protocol occur via the Pool contract

Discovered Vulnerabilities

Certora tested a total of 46 rules written in CVL during their campaign, with an accompanying manual review of the code included as well, the resulting vulnerabilities identified from these efforts are shown here:

  • 1 Critical
  • 2 High
  • 2 Medium 
  • 1 Low
  • 1 Recommendation

For this post we'll focus on one of the High severity vulnerabilities detected using a rule during the Certora review, you can review the other vulnerabilities in the report.

**Note**

The spec files used in the report have been updated in the current Aave repo and the vulnerability has been resolved by this PR. For our study, we have created this repo forked from commit d66a5fd (the last commit made before the PR that resolved the issue), so that the tests could be run to show the vulnerability. To run the test for yourself follow the instructions in this README.

H2 - Loss of assets

From the report, we see Certora's description of the issue as: "Confusion of Asset and EMode price feed for liquidations" meaning that the wrong price feed is used for liquidations. In Aave, a user's collateralized debt position (CDP) can be liquidated by anyone using  liquidationCall whenever the health factor (a measure of the debt-to-collateral ratio of their position) is less than 1, we can see that this issue must come from calls to this liquidation function.

Looking at the liquidationCall function in the Pool contract:

  function liquidationCall(
    address collateralAsset,
    address debtAsset,
    address user,
    uint256 debtToCover,
    bool receiveAToken
  ) external override {
    LiquidationLogic.executeLiquidationCall(
      _reserves,
      _usersConfig,
      _reservesList,
      _eModeCategories,
      DataTypes.ExecuteLiquidationCallParams({
        reservesCount: _reservesCount,
        debtToCover: debtToCover,
        collateralAsset: collateralAsset,
        debtAsset: debtAsset,
        user: user,
        receiveAToken: receiveAToken,
        priceOracle: ADDRESSES_PROVIDER.getPriceOracle(),
        userEModeCategory: _usersEModeCategory[user],
        priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel()
      })
    );
  }

we can see that the price oracle passed into ExecuteLiquidationCallParams is returned by the getPriceOracle function in PoolAddressProvider contract:

 function getPriceOracle() external view override returns (address) {
    return getAddress(PRICE_ORACLE);
  }

This is the default asset price oracle, meaning that if the user is in eMode ("efficiency mode" which is supposed to maximize capital efficiency and allows using a specific price oracle for each eMode category) an oracle other than the default asset oracle that might've been specified will be ignored.

Given that the user can be using any number of a possible 255 eMode categories, each with a possible price oracle, the priceOracle parameter passed into the ExecuteLiquidationCallParams should check if the category being used has a different oracle associated with it and only fallback to the default oracle if none has been specified. 

The property outlined by Certora to catch this vulnerability was "Emode source price usage" which is located in the original pool.spec file under the rule named priceOracelCounter, for this article we have rewritten this rule in the priceOracelCounter.spec file for clarity and to resolve some issues with the Certora Prover due to the specification used in the review being written in an older version of CVL. The slightly modified rule is defined as: 

using SymbolicPriceOracle as priceOracle;

ghost mapping(address => mathint) usageOfPriceOracle {
    init_state axiom forall address asset. usageOfPriceOracle[asset] == 0;
    axiom forall address asset. usageOfPriceOracle[asset] >= 0;
}

hook Sload uint256 p priceOracle.price[KEY address asset] STORAGE {
	usageOfPriceOracle[asset] = usageOfPriceOracle[asset] + 1;
}

rule priceOracelCounter(address asset, method f) {
	env e;
	calldataarg args;
	mathint before = usageOfPriceOracle[asset];
	f(e, args);
	assert usageOfPriceOracle[asset] == before;
}

where we use the usageOfPrice ghost variable to indicate whether the priceOracle variable has been accessed via a hook into load operations for this variable in the SymbolicPriceOracle contract which we are using as a mock price oracle.  We can see from the below result of running the Prover that it has been violated in the liquidationCall function in the Pool contract:

This proves that the property outlined in the report is violated, meaning that instead of accessing the eMode price oracle associated with the category, the default asset price oracle was used instead. This could potentially cause the liquidation price to be worse than expected and is inconsistent with borrow/repay operations that may use the eMode price oracle.

Conclusion 

We've seen how defining a system's properties via property testing with Certora can uncover novel vulnerabilities that might not have been identified otherwise. Defining these properties as code rather than just having them in writing allows them to become an integral part of the development lifecycle and ensures they are never violated. Once these tests are added to a project's test suite, they can be used in a CI/CD pipeline and run against new changes to ensure that the additions don't cause any breaking of the properties and potential vulnerabilities associated with them.

 

By using this website you agree to our Cookie Policy.

Cookie Settings

We use cookies to improve user experience. Choose what cookie categories you allow us to use. You can read more about our Cookie Policy by clicking on Cookie Policy below.

These cookies enable strictly necessary cookies for security, language support and verification of identity. These cookies can’t be disabled.

These cookies collect data to remember choices users make to improve and give a better user experience. Disabling can cause some parts of the site to not work properly.

These cookies help us to understand how visitors interact with our website, help us measure and analyze traffic to improve our service.

These cookies help us to better deliver marketing content and customized ads.