Aave Protocol: Security Assessment
Aave Protocol: Security Assessment
Aave Protocol
Security Assessment
September 25, 2019
Prepared For:
Emilio Frangella | Aave
[email protected]
Stani Kulechov | Aave
[email protected]
Prepared By:
Gustavo Grieco | Trail of Bits
[email protected]
Dominik Czarnota | Trail of Bits
[email protected]
Michael Colburn | Trail of Bits
[email protected]
Changelog:
September 6, 2019: Initial report delivered
September 25, 2019: Added Appendix E with retest results
October 23, 2019: Updated product name from DLPs to Aave Protocol
Executive Summary
Project Dashboard
Engagement Goals
Coverage
Recommendations Summary
Short Term
Long Term
Findings Summary
1. Solidity compiler optimizations can be dangerous
2. Lack of access controls in updateReserveTotalBorrowsByRateMode
3. Lack of access control for LendingPoolAddressesProvider and
NetworkMetadataProvider
4. Lack of access control for FeeProvider setter functions
5. borrow/repay calls race condition can be exploited to repay more than expected
6. repay calls can be blocked by front-running them with another repay call
7. repay does not validate if the user borrows ERC20 tokens, but pays using ether
8. redeem does not properly validate parameters and allows drainage of funds
9. updateReserveTotalBorrowsByRateMode does not properly validate
InterestRateMode inputs
10. DefaultReserveInterestRateStrategy parameters are not validated and can block the
Lending Pool
11. There is no way to redeem all available tokens when Lending Pool parameters are
close to the redemption limit
12. Race condition on LendingPool allows attacker to redeem their token first
13. Insufficient validation of reserve address in some LendingPoolCore functions
14. Users should check that test mode is disabled before interacting with the Lending
Pool
15. swapBorrowRateMode does not check if reserve allows fixed interest rates
A. Vulnerability Classifications
E. Fix Log
Detailed Fix Log
Detailed Issue Discussion
© 2019 Trail of Bits Aave Protocol Assessment | 1
Executive Summary
From August 26 through September 6, 2019, Aave engaged Trail of Bits to review the
security of its Aave Protocol smart contracts. Trail of Bits conducted this assessment over
the course of four person-weeks with three engineers working from commit hash
493d75db59de33ad1f2bb13612ff8acc8654dd3d from the aave-tech/dlp repository. Aave
developed a decentralized lending pool (DLP) protocol in Solidity that allows users to
deposit, borrow, and repay loans, using different types of compounding interest rates. The
system keeps tracks of the liquidity and health metrics to make sure users can only
perform certain operations (e.g., borrow or redeem) with acceptable levels of risk.
During the first week, the assessment focused on Aave Protocol’s smart contracts. This
included a broad pass of the codebase to familiarize ourselves with its architecture, as well
as a specific focus on core functions. We dedicated the second week to investigating
high-level interactions of benign and malicious users within the Lending Pool to identify
potential attacks like front-running and race conditions. We also evaluated the potential
use of Echidna, our properly-based testing tool (Appendix C) and provided feedback on the
whitepaper and its corresponding code (Appendix D).
During the engagement, Trail of Bits identified 15 issues, ranging in severity from
informational to high. We discovered:
● Issues related to missing access control code that is still in development or needs to
be improved.
● Issues in the code related to insufficient data validation of untrusted inputs. This
opened the door to several attacks to drain the available funds with a call to redeem
or subvert normal behavior by changing the interest rates of the Lending Pool.
● Issues with race conditions and potential front-running attacks that may trick users
into paying more than expected, or produce a denial-of-service of certain
operations.
● Issues related to unsafe compiler optimizations, potentially invalid contract
initialization, and leftover code used only for debugging.
Overall, the code reviewed is indicative of a work in progress. A significant portion of
functionality, including distributed governance and the associated access control
mechanisms, is still in development and will likely change in the future. The Aave Protocol
is a large code base, comprised of an extensive amount of corner cases throughout the
protocol.
Aave Protocol requires further development before it is mature enough for production
deployment. Documentation of the expected system operation and environment is
necessary. The Aave team is aware of gaps in its testing suite and is working to improve
© 2019 Trail of Bits Aave Protocol Assessment | 2
test coverage. Once this is completed, Trail of Bits recommends further assessment to
re-evaluate the security posture of the system.
Update: From September 23 to September 25, 2019, Trail of Bits reviewed fixes proposed by Aave
for the issues presented in this report. See a detailed review of the current status of each issue in
Appendix E.
© 2019 Trail of Bits Aave Protocol Assessment | 3
Project Dashboard
Application Summary
Name Aave Protocol
Version 493d75db59de33ad1f2bb13612ff8acc8654dd3d
© 2019 Trail of Bits Aave Protocol Assessment | 4
Engagement Goals
Trail of Bits and Aave scoped the engagement to provide a security assessment of the Aave
Protocol smart contracts in the aave-tech/dlp repository.
Specifically, we sought to answer the following questions:
● Is it possible for the protocol to lose money?
● Can interest rates be manipulated?
● Can reserve data be manipulated?
● Are access controls well defined?
● Is it possible to manipulate the market by using specially crafted parameters or
front-running transactions?
● Is it possible for participants to steal or lose tokens?
● Can participants perform denial-of-service attacks against any of the contracts?
Coverage
This review included the contracts comprising the Aave Protocol. The specific version of the
codebase used for the assessment came from commit hash
493d75db59de33ad1f2bb13612ff8acc8654dd3d of the aave-tech/dlp Gitlab repository.
Trail of Bits reviewed contracts for common Solidity flaws, such as integer overflows,
reentrancy vulnerabilities, and unprotected functions.
Arithmetic. Complex calculations, often including fixed-point math, take place in many
parts of the system. We reviewed these calculations for logical consistency and any issues
that may result from rounding or overflow.
Access controls. Some parts of the system expose privileged functionality, such as
modifying borrow rates or fees. We reviewed these functions to ensure they can only be
triggered by the intended actors. However, as distributed governance was not
implemented at the time of the assessment, it was not possible to review the adequacy of
some of these mechanisms (e.g., TOB-DLP-003).
LendingPoolCore. We gave special attention to the core lending pool contract, as that is
one of the few non-upgradable components of the Aave Protocol and contains important
logic.
Market manipulation. Participants may manipulate market parameters, time their
interactions, or collude with other participants to profit from crypto-economic incentives
© 2019 Trail of Bits Aave Protocol Assessment | 5
underlying the markets. In reviewing the design and implementation, we assumed any
participant may be untrustworthy and malicious.
The distributed governance and the collateralCall functionality were still a work in
progress and thus, out of scope of this assessment.
This engagement did not have sufficient time for review of the testing suite or deployment
scripts. Aave also noted that several issues from a recent security assessment of
Compound Finance may be relevant to their protocol. As the team was already aware of
these issues, coverage was focused elsewhere and they were not included in this report.
© 2019 Trail of Bits Aave Protocol Assessment | 6
Recommendations Summary
This section aggregates all the recommendations made during the engagement. Short-term
recommendations address the immediate causes of issues. Long-term recommendations
pertain to the development process and long-term design goals.
Short Term
❑ Measure the gas savings from optimizations, and carefully weigh that against the
possibility of an optimization-related bug. Disabling optimization will mitigate
compilation for present and future issues introduced by the compiler.
❑ Use the onlyLendingPool modifier with the updateReserveTotalBorrowsByRateMode
function to restrict the access to that code. It will avoid unauthorized users changing
critical state variables in the Lending Pool.
❑ Protect setter operations in LendingPoolAddressesProvider,
NetworkMetadataProvider, and their base contracts (AddressStorage and UintStorage)
from being called by unexpected addresses. It will avoid unauthorized users changing
critical state variables in the Lending Pool
❑ Document potential race conditions and front-running attacks to make sure users
are aware of this risk when they use redeem, borrow, or repay. It will be easier to
report and identify these types of attacks if users are aware of them.
❑ Properly validate the amount of ETH sent to the Lending Pool during a repay. It will
avoid trapping user funds that were sent by mistake.
❑ Properly validate the provided aToken address using the corresponding reserve. It
will prevent using untrusted addresses to read the amount of tokens available to redeem.
❑ Properly validate the interestRateMode inputs to allow only valid modes for each
reserve. It will prevent the use of restricted interest modes from certain reserves.
❑ Properly define input ranges, and validate the
DefaultReserveInterestRateStrategy contract parameters on-chain. It will prevent
invalid setup parameters that can cause unexpected behavior in the Lending Pool.
❑ Properly implement a mechanism to redeem all the available tokens, given the
current liquidity and health liquidation factor. Alternatively, document the
constraints to redeem tokens under situations of limited liquidity or low health
liquidation factor. This will help users to redeem as many tokens as possible during a
bank run.
© 2019 Trail of Bits Aave Protocol Assessment | 7
❑ Add missing validation checks to the relevant functions in LendingPoolCore. This
will prevent using uninitialized values when the reserve values have not been previously
initialized.
❑ Remove the test mode, or refactor it to a separate contract that uses inheritance
to expose this special function. This will prevent users from distrusting the Lending Pool
if they notice this special testing mode in the deployed contract.
© 2019 Trail of Bits Aave Protocol Assessment | 8
Long Term
❑ Monitor the development and adoption of Solidity compiler optimizations to
assess its maturity. This will avoid introducing any known issues caused by the compiler
during future development.
❑ Add proper unit tests for corner cases in which a user tries to perform an invalid or
unauthorized operation, including:
● Configuration contracts cannot be deployed with invalid parameters.
● A user cannot successfully call any state-modifying function in the LendingPoolCore
contract.
● A user cannot successfully call any Lending Pool function using an arbitrary address
as reserve or token contracts.
● The LendingPool contract cannot successfully take ETH and ERC20 tokens from a
user at the same time.
❑ Implement a governance system to agree on changes. This will address the access
control issues detected during this assessment.
❑ Consider requesting the user input an upper bound of the price he is willing to
repay. Revert if the load is higher than expected, or remove the feature to repay the
entire loan using uint(-1). This will mitigate a race condition in the use of borrow/repay.
❑ Consider monitoring the blockchain using the Repay events. This will help catch
front-running attacks.
❑ Take into account that all information sent to Ethereum is public prior being
accepted by the network. Design the contracts to be robust to the unpredictable nature
of transaction ordering.
❑ Always ensure data has been initialized before accessing it. This will avoid
unexpected behavior when using uninitialized values.
❑ Review all capabilities in your smart contracts to make sure you are not including
any debugging code that could be used as a backdoor. This will prevent users from
distrusting your contracts if they notice debugging code in the deployed contract.
❑ Consider using Echidna and Manticore to test important properties of the Lending
Pool, including:
● Deploying of configuration contracts will only valid parameters.
● User can redeem the maximum amount of their token using redeem.
● Users cannot change the interest mode of their loans if the reserve has restrictions.
© 2019 Trail of Bits Aave Protocol Assessment | 9
Findings Summary
# Title Type Severity
© 2019 Trail of Bits Aave Protocol Assessment | 10
12 Race condition on LendingPool allows Timing Medium
attacker to redeem their token first
14 Users should check that test mode is Access Controls Informational
disabled before interacting with the
Lending Pool
© 2019 Trail of Bits Aave Protocol Assessment | 11
1. Solidity compiler optimizations can be dangerous
Severity: Undetermined Difficulty: Low
Type: Undefined Behavior Finding ID: TOB-DLP-01
Target: truffle-config.js
Description
The Aave Protocol contracts have enabled optional compiler optimizations in Solidity.
There have been several bugs with security implications related to optimizations.
Moreover, optimizations are actively being developed. Solidity compiler optimizations are
disabled by default. It is unclear how many contracts in the wild actually use them.
Therefore, it is unclear how well they are being tested and exercised.
High-severity security issues due to optimization bugs have occurred in the past. A
high-severity bug in the emscripten-generated solc-js compiler used by Truffle and Remix
persisted until late 2018. The fix for this bug was not reported in the Solidity CHANGELOG.
Another high-severity optimization bug resulting in incorrect bit shift results was patched in
Solidity 0.5.6.
A compiler audit of Solidity from November 2018 concluded that the optional optimizations
may not be safe. Moreover, the Common Subexpression Elimination (CSE) optimization
procedure is “implemented in a very fragile manner, with manual access to indexes,
multiple structures with almost identical behavior, and up to four levels of conditional
nesting in the same function.” Similar code in other large projects have resulted in bugs.
There are likely latent bugs related to optimization and/or new bugs that will be introduced
due to future optimizations.
Exploit Scenario
A latent or future bug in Solidity compiler optimizations—or in the Emscripten transpilation
to solc-js—causes a security vulnerability in the Aave Protocol contracts.
Recommendation
In the short term, measure the gas savings from optimizations, and carefully weigh that
against the possibility of an optimization-related bug.
In the long term, monitor the development and adoption of Solidity compiler optimizations
to assess its maturity.
© 2019 Trail of Bits Aave Protocol Assessment | 12
2. Lack of access controls in updateReserveTotalBorrowsByRateMode
Severity: High Difficulty: Low
Type: Access Controls Finding ID: TOB-DLP-02
Target: LendingPoolCore.sol
Description
updateReserveTotalBorrowsByRateMode modifies critical state variables, but it is not
protected and can be called by any user.
The updateReserveTotalBorrowsByRateMode function (Figure 2.1) is used to update the
borrow information for an arbitrary reserve and user.
function updateReserveTotalBorrowsByRateMode(
address _reserve,
address _user,
uint256 _principalBalance,
uint256 _balanceIncrease,
uint256 _newBorrowRateMode,
uint256 _fixedRate
) external {
CoreLibrary.InterestRateMode currentRateMode =
getUserCurrentBorrowRateMode(_reserve, _user);
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
if (currentRateMode == CoreLibrary.InterestRateMode.FIXED) {
// there was no previous borrow or previous borrow was fixed
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
reserve.decreaseTotalBorrowsFixedAndUpdateAverageRate(_principalBalance,
user.fixedBorrowRate);
if (_newBorrowRateMode == uint256(CoreLibrary.InterestRateMode.FIXED)) {
reserve.increaseTotalBorrowsFixedAndUpdateAverageRate(_principalBalance.add(_balanceIncreas
e), _fixedRate);
} else if (_newBorrowRateMode ==
uint256(CoreLibrary.InterestRateMode.VARIABLE)) {
//switching the whole amount borrowed to variable
reserve.increaseTotalBorrowsVariable(_principalBalance.add(_balanceIncrease));
}
© 2019 Trail of Bits Aave Protocol Assessment | 13
} else {
// previous borrow was variable
if (_newBorrowRateMode == uint256(CoreLibrary.InterestRateMode.FIXED)) {
reserve.decreaseTotalBorrowsVariable(_principalBalance);
reserve.increaseTotalBorrowsFixedAndUpdateAverageRate(_principalBalance.add(_balanceIncreas
e), _fixedRate);
} else if (_newBorrowRateMode ==
uint256(CoreLibrary.InterestRateMode.VARIABLE)) {
//switching the whole amount borrowed to variable
reserve.increaseTotalBorrowsVariable(_balanceIncrease);
}
}
}
Figure 2.1: updateReserveTotalBorrowsByRateMode function in LendingPoolCore.sol
This function should only be called by the LendingPool contract; however, it currently can
be called by any user.
Exploit Scenario
An attacker can directly manipulate the LendingPoolCore state variables, using
updateReserveTotalBorrowsByRateMode to exploit the LendingPool contract.
Recommendation
Short term, use the onlyLendingPool modifier with the
updateReserveTotalBorrowsByRateMode function to restrict the access to that code.
Long term, add proper unit tests to ensure that an unauthorized user cannot successfully
call any state-modifying function in the LendingPoolCore contract.
© 2019 Trail of Bits Aave Protocol Assessment | 14
3. Lack of access control for LendingPoolAddressesProvider and
NetworkMetadataProvider
Severity: Medium Difficulty: Low
Type: Access Controls Finding ID: TOB-DLP-03
Target: LendingPoolAddressesProvider.sol, NetworkMetadataProvider.sol
Description
Both LendingPoolAddressesProvider and NetworkMetadataProvider contracts lack
access controls for operations that set address or uint256. This allows an attacker to
change the behavior of a given AToken, LendingPool, LendingPoolCore or
LendingPoolConfigurator by changing relevant addresses or uint values.
This issue seems to be known due to “// TODO: add access control rules under DAO”
comments accompanying the code of the affected functions. However, this access control
scheme was not yet implemented at the time of the assessment.
Exploit Scenario
An attacker changes LendingPoolCore address of LendingPool contract’s address provider
(LendingPoolAddressesProvider) to another contract they control. This contract can, for
example, revert all calls made to it, causing LendingPool contract’s operations to fail.
Recommendation
Short term, protect setter operations from being called by unexpected addresses. The
setter functions that need to be protected are in LendingPoolAddressesProvider,
NetworkMetadataProvider, and their base contracts (AddressStorage and UintStorage).
This can be done using an Ownable contract and setting the owner to a multi-signature
wallet, which will avoid a single point of failure, in case of a private-key compromise.
Long term, implement a governance system to agree on changes.
© 2019 Trail of Bits Aave Protocol Assessment | 15
4. Lack of access control for FeeProvider setter functions
Severity: High Difficulty: Low
Type: Access Controls Finding ID: TOB-DLP-04
Target: FeeProvider.sol
Description
The setFeesCollectionAddress and setLoanOriginationFeePercentage functions in
FeeProvider contract can be called by anyone. This allows an attacker to modify the loan
origination fee percentage and the address to which the fees are sent.
Exploit Scenario
An attacker changes the feesCollectionAddress of a FeeProvider used by a LendingPool
by calling the setFeesCollectionAddress function. As a result, all collected fees are now
sent to their address, instead of the original one.
Recommendation
Short term, protect setter operations from being called by unexpected addresses. The
setter functions that needs to be protected are setFeesCollectionAddress and
setLoanOriginationFeePercentage in FeeProvider contract.
This can be done using an Ownable contract and setting the owner to a multi-signature
wallet, which will avoid a single point of failure, in case of a private-key compromise.
Long term, implement a governance system to agree on changes.
© 2019 Trail of Bits Aave Protocol Assessment | 16
5. borrow/repay calls race condition can be exploited to repay more than
expected
Severity: High Difficulty: High
Type: Timing Finding ID: TOB-DLP-05
Target: LendingPool.sol
Description
A borrower can front-run a repay with another borrow, forcing the user to repay more than
expected.
The repay function (Figure 5.1) is used to return the funds borrowed, even on behalf of
other users.
function repay(address _reserve, uint256 _amount, address _onBehalfOf)
external payable nonReentrant {
uint256 compoundedBorrowBalance =
core.getUserCompoundedBorrowBalance(_reserve, _onBehalfOf);
uint256 principalBalance =
core.getUserPrincipalBorrowBalance(_reserve, _onBehalfOf);
uint256 originationFee = core.getUserOriginationFee(_reserve,
_onBehalfOf);
require(
compoundedBorrowBalance.add(originationFee) >= _amount ||
_amount == UINT_MAX_VALUE,
"User is trying to repay more than the total amount of the
loan"
);
…
Figure 5.1: Header of the repay function in LendingPool.sol
This function requires specifying an amount to repay. If this amount is uint(-1), it will
repay the current loan. Otherwise, it will check whether the amount to pay is equal or less
than the loan. If it is equal to the loan, it is possible for an attacker to make another user to
repay more than expected by front-running the repay with a borrow.
Exploit Scenario
Alice wants to repay Bob's loan for 1000 ERC20 tokens. She calls repay using uint(-1) as
the amount. Bob sees the unconfirmed transaction and front-runs it to borrow another
© 2019 Trail of Bits Aave Protocol Assessment | 17
1000 ERC20 tokens. As a result, LendingPool will take 2000 ERC20 tokens from Alice to
repay, instead of the 1000 ERC20 tokens as Alice intended.
Recommendation
Short term, one possible mitigation is to instruct the users to document this behavior to
make sure users are aware of this risk.
Long term, consider requesting the user input an upper bound of the price that he is willing
to repay. Revert if the load is higher than expected. Alternatively, remove the feature to
repay the entire loan using uint(-1).
© 2019 Trail of Bits Aave Protocol Assessment | 18
6. repay calls can be blocked by front-running them with another repay call
Severity: Low Difficulty: High
Type: Timing Finding ID: TOB-DLP-06
Target: LendingPool.sol
Description
An attacker can make a repay call to revert by front-running it with another repay, forcing
the user to redo the transaction.
The repay function (Figure 6.1) is used to return the funds borrowed, even on behalf of
other users.
function repay(address _reserve, uint256 _amount, address _onBehalfOf)
external payable nonReentrant {
uint256 compoundedBorrowBalance =
core.getUserCompoundedBorrowBalance(_reserve, _onBehalfOf);
uint256 principalBalance =
core.getUserPrincipalBorrowBalance(_reserve, _onBehalfOf);
uint256 originationFee = core.getUserOriginationFee(_reserve,
_onBehalfOf);
require(
compoundedBorrowBalance.add(originationFee) >= _amount ||
_amount == UINT_MAX_VALUE,
"User is trying to repay more than the total amount of the
loan"
);
…
Figure 6.1: Header of the repay function in LendingPool.sol
This function requires specifying an amount to repay. If this amount is uint(-1), it will
repay the current loan. Otherwise, it will check whether the amount to pay is equal or less
than the loan. If it is less than the loan, it is possible for an attacker to block the transaction
by front-running the repay with another one.
Exploit Scenario
Alice wants to repay her 10 ETH loan. She calls repay with the total amount. Bob sees the
unconfirmed transaction and front-runs it with another repay on her behalf, but using the
minimal amount (e.g. 1 WEI). As a result, Alice’s transaction reverts, since her loan is less
than 10 ETH. She now needs to redo the repay.
© 2019 Trail of Bits Aave Protocol Assessment | 19
Recommendation
Short term, as a simpler alternative, document this behavior, advising users to carefully
approve a tight amount of tokens for the LendingPool.
Long term, consider monitoring the blockchain using the Repay events to catch this
possible attack.
© 2019 Trail of Bits Aave Protocol Assessment | 20
7. repay does not validate if the user borrows ERC20 tokens, but pays using
ether
Severity: Low Difficulty: High
Type: Data Validation Finding ID: TOB-DLP-07
Target: LendingPool.sol
Description
A user sending ETH to repay ERC20 tokens will lose the ETH sent.
The repay function (Figure 7.1) can be used to repay ETH or ERC20 tokens. The call to the
transferToReserve function (Figure 7.2) is responsible to validate the amount of
ether/tokens.
function repay(address _reserve, uint256 _amount, address _onBehalfOf)
external payable nonReentrant {
uint256 compoundedBorrowBalance =
core.getUserCompoundedBorrowBalance(_reserve, _onBehalfOf);
uint256 principalBalance =
core.getUserPrincipalBorrowBalance(_reserve, _onBehalfOf);
uint256 originationFee = core.getUserOriginationFee(_reserve,
_onBehalfOf);
require(
compoundedBorrowBalance.add(originationFee) >= _amount ||
_amount == UINT_MAX_VALUE,
"User is trying to repay more than the total amount of the
loan"
);
…
core.transferToReserve.value(msg.value)(_reserve, _onBehalfOf,
paybackAmountMinusFees);
© 2019 Trail of Bits Aave Protocol Assessment | 21
function transferToReserve(address _reserve, address _user, uint256
_amount) external payable onlyLendingPool {
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
require(
reserve.usageAsCollateralEnabled || reserve.borrowingEnabled,
"The reserve isn't enabled for borrowing or as collateral"
);
if (_reserve != ethereumAddress)
ERC20(_reserve).safeTransferFrom(_user, address(this),
_amount);
else require(msg.value == _amount, "The amount and the value sent
to deposit do not match");
}
Figure 7.2: transferToReserve in LendingPoolCore.sol.
The function will take the ERC20 tokens and accept the ETH, without reporting any error.
Exploit Scenario
Alice mistakenly tries to pay her ERC20 tokens loan, using ETH. As a result, the LendingPool
contract will take her ERC20 tokens, and she will lose her ETH.
Recommendation
Short term, disallow sending ETH if the user has an ERC20 token loan.
Long term, add proper unit tests to ensure that the LendingPool contract cannot
successfully take ETH and ERC20 tokens from a user at the same time.
© 2019 Trail of Bits Aave Protocol Assessment | 22
8. redeem does not properly validate parameters and allows drainage of
funds
Severity: High Difficulty: Low
Type: Data Validation Finding ID: TOB-DLP-08
Target: LendingPool.sol
Description
An attacker could use the redeem function with a specially crafted AToken contract to steal
all the available liquidity.
The redeem function (Figure 8.1) allows users to get back their funds, given the address of a
aToken contract and the amount to redeem.
function redeem(address _aToken, uint256 _aTokenAmount) external nonReentrant {
uint256 underlyingAmountToRedeem = 0;
uint256 aTokenBalance = aToken.balanceOf(msg.sender);
require(
aTokenBalance >= _aTokenAmount && _aTokenAmount > 0,
"The aToken user balance is not enough to cover the amount to redeem or the
amount to redeem is 0"
);
//at this point, the withdraw should not trigger the liquidation
require(
healthFactor > dataProvider.getHealthFactorLiquidationThreshold(),
"Redeem would bring the loan in a liquidation state thus cannot be accepted"
);
/**
@dev update reserve data
*/
core.decreaseReserveTotalLiquidity(reserve, underlyingAmountToRedeem);
core.updateReserveInterestRates(reserve);
core.setReserveLastUpdate(reserve);
© 2019 Trail of Bits Aave Protocol Assessment | 23
emit Redeem(_aToken, msg.sender, _aTokenAmount, underlyingAmountToRedeem);
}
Figure 8.1: redeem function in LendingPool.sol
However, since it does not check the validity of the supplied AToken contract, it allows any
user to control the amount of liquidity to redeem in the underlyingAmountToRedeem
variable. bec
Exploit Scenario
An attacker can create a specially crafted AToken contract to redeem any user funds, given
there is enough liquidity. Such contract just return arbitrary amounts when the balanceOf
and the aTokenAmountToUnderlyingAmount functions are called, in order to perform the
exploit.
Recommendation
Short term, properly validate the provided _aToken address using the corresponding
reserve.
Long term, add proper unit tests to simulate this attack, in order to test that current redeem
implementation is not vulnerable.
© 2019 Trail of Bits Aave Protocol Assessment | 24
9. updateReserveTotalBorrowsByRateMode does not properly validate
InterestRateMode inputs
Severity: High Difficulty: Low
Type: Data Validation Finding ID: TOB-DLP-09
Target: LendingPool.sol
Description
The interest rate mode parameter is not properly validated during the call to borrow and
can leave the contract in an inconsistent state.
The borrow function allows borrowers to specify an interestRateMode input, which could
be either FIXED (0) or VARIABLE (1).
function borrow(address _reserve, uint256 _amount, uint256
_interestRateMode, uint16 _referralCode) external nonReentrant {
BorrowLocalVars memory vars;
© 2019 Trail of Bits Aave Protocol Assessment | 25
uint256 _fixedRate
) external onlyLendingPool {
CoreLibrary.InterestRateMode currentRateMode =
getUserCurrentBorrowRateMode(_reserve, _user);
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
if (currentRateMode == CoreLibrary.InterestRateMode.FIXED) {
// there was no previous borrow or previous borrow was fixed
CoreLibrary.UserReserveData storage user =
usersReserveData[_user][_reserve];
reserve.decreaseTotalBorrowsFixedAndUpdateAverageRate(_principalBalance,
user.fixedBorrowRate);
if (_newBorrowRateMode ==
uint256(CoreLibrary.InterestRateMode.FIXED)) {
reserve.increaseTotalBorrowsFixedAndUpdateAverageRate(_principalBalance.ad
d(_balanceIncrease), _fixedRate);
} else if (_newBorrowRateMode ==
uint256(CoreLibrary.InterestRateMode.VARIABLE)) {
//switching the whole amount borrowed to variable
reserve.increaseTotalBorrowsVariable(_principalBalance.add(_balanceIncreas
e));
}
} else {
// previous borrow was variable
if (_newBorrowRateMode ==
uint256(CoreLibrary.InterestRateMode.FIXED)) {
reserve.decreaseTotalBorrowsVariable(_principalBalance);
reserve.increaseTotalBorrowsFixedAndUpdateAverageRate(_principalBalance.ad
d(_balanceIncrease), _fixedRate);
} else if (_newBorrowRateMode ==
uint256(CoreLibrary.InterestRateMode.VARIABLE)) {
//switching the whole amount borrowed to variable
reserve.increaseTotalBorrowsVariable(_balanceIncrease);
}
}
}
Figure 9.2: updateReserveTotalBorrowsByRateMode function in LendingPoolCore.sol
However, this function does not properly validate the borrow-rate mode used. Therefore, if
an invalid input is used, the code will skip important code which modifies the state
variables.
© 2019 Trail of Bits Aave Protocol Assessment | 26
Exploit Scenario
An attacker borrows funds, using an invalid borrow-rate mode to force the contract to
transition to an invalid state. As a result, the contract must be patched and re-deployed.
Recommendation
Short term, properly validate the _interestRateMode input to allow only valid modes.
Long term, add proper unit tests to simulate this attack and test that the current borrow
implementation is not vulnerable.
© 2019 Trail of Bits Aave Protocol Assessment | 27
10. DefaultReserveInterestRateStrategy parameters are not validated and
can block the Lending Pool
Severity: Low Difficulty: High
Type: Data Validation Finding ID: TOB-DLP-10
Target: DefaultReserveInterestRateStrategy.sol
Description
The lack of parameter validation in DefaultReserveInterestRateStrategy can block the
Lending Pool, if invalid values are used.
The DefaultReserveInterestRateStrategy contract requires a number of parameters to
be deployed:
contract DefaultReserveInterestRateStrategy is
IReserveInterestRateStrategy {
LendingPoolCore core;
ILendingRateOracle lendingRateOracle;
uint256 baseVariableBorrowRate;
uint256 variableBorrowRateScaling;
uint256 fixedBorrowRateScaling;
uint256 borrowToLiquidityRateDelta;
address reserve;
constructor(
address _reserve,
LendingPoolAddressesProvider _provider,
uint256 _baseVariableBorrowRate,
uint256 _variableBorrowRateScaling,
uint256 _fixedBorrowRateScaling,
uint256 _borrowToLiquidityRateDelta) public {
core = LendingPoolCore(_provider.getLendingPoolCore());
lendingRateOracle =
ILendingRateOracle(_provider.getLendingRateOracle());
baseVariableBorrowRate = _baseVariableBorrowRate;
variableBorrowRateScaling = _variableBorrowRateScaling;
fixedBorrowRateScaling = _fixedBorrowRateScaling;
© 2019 Trail of Bits Aave Protocol Assessment | 28
borrowToLiquidityRateDelta = _borrowToLiquidityRateDelta;
reserve = _reserve;
}
…
Figure 10.1: Header of the DefaultReserveInterestRateStrategy contract in
DefaultReserveInterestRateStrategy.sol
These parameters are copied into the state variables and used in several important
functions, including calculateInterestRates:
function calculateInterestRates(
address _reserve,
uint256 _utilizationRate,
uint256 _totalBorrowsFixed,
uint256 _totalBorrowsVariable,
uint256 _averageFixedBorrowRate
) external view returns (uint256 currentLiquidityRate, uint256
currentFixedBorrowRate, uint256 currentVariableBorrowRate) {
currentFixedBorrowRate =
lendingRateOracle.getMarketBorrowRate(_reserve);
currentVariableBorrowRate =
_utilizationRate.rayMul(variableBorrowRateScaling).add(
baseVariableBorrowRate
);
currentLiquidityRate = getOverallBorrowRateInternal(
_totalBorrowsFixed,
_totalBorrowsVariable,
currentVariableBorrowRate,
_averageFixedBorrowRate).rayMul(_utilizationRate);
}
Figure 10.2: calculateInterestRates function in DefaultReserveInterestRateStrategy.sol
However the parameters are not validated and cannot be changed by anyone (not even the
owner of the contract), if they need to be adjusted. If the parameters use some invalid
values, the call to calculateInterestRates could revert, potentially blocking the
LendingPool contract.
© 2019 Trail of Bits Aave Protocol Assessment | 29
Exploit Scenario
The DefaultReserveInterestRateStrategy is deployed using some invalid parameters,
causing unexpected reverts in calculateInterestRates. As a result, the important functions
in the Lending Pool such as borrow, deposit, redeem and repay will revert, forcing the
Lending Pool to be patched and re-deployed.
Recommendation
Short term, properly define input ranges and validate the
DefaultReserveInterestRateStrategy contract parameters on-chain.
Long term, add proper unit tests to ensure that DefaultReserveInterestRateStrategy
deployment will fail with invalid parameters. Consider using Echidna and Manticore to test
the constructor implementation and make sure it will only accept valid parameters.
© 2019 Trail of Bits Aave Protocol Assessment | 30
11. There is no way to redeem all available tokens when Lending Pool
parameters are close to the redemption limit
Severity: Medium Difficulty: Low
Type: Data Validation Finding ID: TOB-DLP-11
Target: LendingPool.sol
Description
If the available liquidity or health metric are close to the acceptable limits, the redeem
function provides no way to specify to redeem the maximum amount of available tokens,
forcing the user to guess this amount.
Users can get back their deposited funds using the redeem function:
function redeem(address _aToken, uint256 _aTokenAmount) external
nonReentrant {
uint256 underlyingAmountToRedeem = 0;
uint256 aTokenBalance = aToken.balanceOf(msg.sender);
require(
aTokenBalance >= _aTokenAmount && _aTokenAmount > 0,
"The aToken user balance is not enough to cover the amount to
redeem or the amount to redeem is 0"
);
uint256 currentAvailableLiquidity =
core.getReserveAvailableLiquidity(reserve);
require(currentAvailableLiquidity >= underlyingAmountToRedeem,
"There is not enough liquidity available to redeem");
(,,,,,uint256 healthFactor) =
dataProvider.calculateUserGlobalData(msg.sender);
//at this point, the withdraw should not trigger the liquidation
require(
© 2019 Trail of Bits Aave Protocol Assessment | 31
healthFactor >
dataProvider.getHealthFactorLiquidationThreshold(),
"Redeem would bring the loan in a liquidation state thus
cannot be accepted"
);
/**
@dev update reserve data
*/
core.decreaseReserveTotalLiquidity(reserve,
underlyingAmountToRedeem);
core.updateReserveInterestRates(reserve);
core.setReserveLastUpdate(reserve);
core.transferToUser(reserve, msg.sender,
underlyingAmountToRedeem);
}
Figure 11.1: redeem function in LendingPool.sol
However, redemption is restricted: it should have enough liquidity available and the health
liquidation factor should be above a certain threshold. When a user tries to redeem all
their tokens while the Lending Pool parameters are close to the redemption limit, it is very
difficult for the user to determine how many tokens can be redeemed. There is no option
to specify to redeem as much as possible, as in the repay function. This can be a significant
obstacle for the user trying to redeem their tokens.
Exploit Scenario
Alice has a large amount of tokens. During a bank run, she tries to redeem all of them. She
repeatedly fails to do so, since she specifies too many tokens to redeem. As a result, Alice is
forced to redeem her tokens in small batches, which costs more gas.
Recommendation
Short term, properly implement a mechanism to redeem all the available tokens, given the
current liquidity and health liquidation factor. As an alternative, document this behavior to
warn users about this constraint.
Long term, consider using Echidna and Manticore to test that users can redeem the
maximum amount of their token using redeem.
© 2019 Trail of Bits Aave Protocol Assessment | 32
12. Race condition on LendingPool allows attacker to redeem their token
first
Severity: Medium Difficulty: High
Type: Timing Finding ID: TOB-DLP-12
Target: LendingPool.sol
Description
A race condition on the LendingPool contract allows an attacker to front-run a large token
redemption during a bank run so he can redeem his own tokens first.
User can get back their deposited funds using the redeem function:
function redeem(address _aToken, uint256 _aTokenAmount) external
nonReentrant {
uint256 underlyingAmountToRedeem = 0;
uint256 aTokenBalance = aToken.balanceOf(msg.sender);
require(
aTokenBalance >= _aTokenAmount && _aTokenAmount > 0,
"The aToken user balance is not enough to cover the amount to
redeem or the amount to redeem is 0"
);
uint256 currentAvailableLiquidity =
core.getReserveAvailableLiquidity(reserve);
require(currentAvailableLiquidity >= underlyingAmountToRedeem,
"There is not enough liquidity available to redeem");
(,,,,,uint256 healthFactor) =
dataProvider.calculateUserGlobalData(msg.sender);
//at this point, the withdraw should not trigger the liquidation
require(
© 2019 Trail of Bits Aave Protocol Assessment | 33
healthFactor >
dataProvider.getHealthFactorLiquidationThreshold(),
"Redeem would bring the loan in a liquidation state thus
cannot be accepted"
);
/**
@dev update reserve data
*/
core.decreaseReserveTotalLiquidity(reserve,
underlyingAmountToRedeem);
core.updateReserveInterestRates(reserve);
core.setReserveLastUpdate(reserve);
core.transferToUser(reserve, msg.sender,
underlyingAmountToRedeem);
}
Figure 12.1: redeem function in LendingPool.sol
In Ethereum, all transactions appear on the network before being accepted. Users can see
upcoming token redemptions. As a result, an attacker can front-run large token
redemptions to make sure his own tokens will be redeemed.
Exploit Scenario
Alice has a large amount of tokens. During a bank run, she tries to redeem all of them. Bob
sees the unconfirmed redeem transaction and front-runs her redeem call with his own
tokens, using a higher gas-price.
Alice’s transaction reverts if the available liquidity or the health liquidity factor are too low,
since Bob’s redemption will be performed first. As a result, Alice’s redeem call reverts, and
she cannot get her funds back.
Recommendation
Short term, document this behavior to make sure users are aware of redemption
front-runs that can be exploited by others. On large redemptions, consider:
● Warning off-chain about this behavior
● Recommending an increase to the gas price or splitting the transaction into several
smaller ones to avoid detection by potential attackers
Long term, carefully consider the unpredictable nature of Ethereum transactions, and
design contracts to not depend on the order of transactions.
© 2019 Trail of Bits Aave Protocol Assessment | 34
13. Insu ficient validation of reserve address in some LendingPoolCore
functions
Severity: Information al Difficulty: Low
Type: Data Validation Finding ID: TOB-DLP-13
Target: LendingPoolCore.sol
Description
Several functions within the LendingPoolCore contract operate on structs stored in
mappings. The _reserve and _user function parameters determine which particular struct
is modified. These functions currently do not validate that the struct being accessed has
been initialized. As a result, a Lending Pool contract may unintentionally access
uninitialized data, and then read or modify it instead of the intended data.
function increaseUserOriginationFee(address _reserve, address _user, uint256 _amount)
external onlyLendingPool {
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
user.originationFee = user.originationFee.add(_amount);
}
Figure 13.2: Further examples of functions which do not validate their input data.
Exploit Scenario
The Aave developers decide to extend the Lending Pool contract, using the
LendingPoolCore functions. However, they do not properly check whether the reserve
address and the returned data from LendingPoolCore are initialized. As a result, the use of
the new code can potentially produce unexpected results.
Recommendations
Short term, add missing validation checks to the relevant functions in LendingPoolCore.
© 2019 Trail of Bits Aave Protocol Assessment | 35
Long term, always ensure data has been initialized before accessing it.
© 2019 Trail of Bits Aave Protocol Assessment | 36
14. Users should check that test mode is disabled before interacting with the
Lending Pool
Severity: Informational Difficulty: High
Type: Access Controls Finding ID: TOB-DLP-14
Target: LendingPoolCore.sol
Description
Users should always check that test mode in the LendingPoolCore is disabled before
interacting with the contracts.
The LendingPoolCore contract defines a special mode called "test mode," which allows any
user to modify the current block number using forwardTime function:
/**
* @notice test function to move forward time, only if testMode is
enabled
*/
© 2019 Trail of Bits Aave Protocol Assessment | 38
15. swapBorrowRateMode does not check if reserve allows fixed interest
rates
Severity: High Difficulty: Low
Type: Access Controls Finding ID: TOB-DLP-15
Target: LendingPool.sol
Description
The swapBorrowRateMode allows changing the interest rate mode of any reserve to fixed,
even if it is disallowed by the reserve itself.
The borrow function allows specifying an interestRateMode input, which could be either
FIXED (0) or VARIABLE (1). Only some reserves allow the fixed interest rate mode.
The swapBorrowRateMode function allows changing from one mode to the other:
function swapBorrowRateMode(address _reserve) external nonReentrant {
CoreLibrary.InterestRateMode currentRateMode =
core.getUserCurrentBorrowRateMode(_reserve, msg.sender);
uint256 compoundedBorrowBalance =
core.getUserCompoundedBorrowBalance(_reserve, msg.sender);
require(compoundedBorrowBalance > 0, "User does not have a borrow
in progress on this reserve");
uint256 currentPrincipalBorrowBalance =
core.getUserPrincipalBorrowBalance(_reserve, msg.sender);
uint256 balanceIncrease =
compoundedBorrowBalance.sub(currentPrincipalBorrowBalance);
if(currentRateMode == CoreLibrary.InterestRateMode.FIXED){
uint256 userFixedRate =
core.getUserCurrentFixedBorrowRate(_reserve, msg.sender);
//switch to variable
core.decreaseReserveTotalBorrowsFixedAndUpdateAverageRate(_reserve,
currentPrincipalBorrowBalance, userFixedRate); //decreasing fixed from old
principal balance
core.increaseReserveTotalBorrowsVariable(_reserve,
compoundedBorrowBalance); //increase variable borrows
© 2019 Trail of Bits Aave Protocol Assessment | 39
core.resetUserFixedBorrowRate(_reserve, msg.sender);
}
else {
//switch to fixed
uint256 currentFixedRate =
core.getReserveCurrentFixedBorrowRate(_reserve);
core.decreaseReserveTotalBorrowsVariable(_reserve,
currentPrincipalBorrowBalance);
core.increaseReserveTotalBorrowsFixedAndUpdateAverageRate(_reserve,
compoundedBorrowBalance, currentFixedRate);
core.updateUserFixedBorrowRate(_reserve,msg.sender);
}
core.increaseReserveTotalLiquidity(_reserve, balanceIncrease);
core.updateReserveInterestRates(_reserve);
© 2019 Trail of Bits Aave Protocol Assessment | 40
A. Vulnerability Classifications
Vulnerability Classes
Class Description
Severity Description
Informational The issue does not pose an immediate risk, but is relevant to security
best practices or Defense in Depth
Undetermined The extent of the risk was not determined during this engagement
Low The risk is relatively small or is not a risk the customer has indicated is
important
© 2019 Trail of Bits Aave Protocol Assessment | 41
implications for client
High Large numbers of users, very bad for client’s reputation, or serious
legal or financial implications
Difficulty Levels
Difficulty Description
Undetermined The difficulty of exploit was not determined during this engagement
Low Commonly exploited, public tools exist or can be scripted that exploit
this flaw
High The attacker must have privileged insider access to the system, may
need to know extremely complex technical details or must discover
other weaknesses in order to exploit this issue
© 2019 Trail of Bits Aave Protocol Assessment | 42
B. Code Quality Recommendations
The following recommendations are not associated with specific vulnerabilities. However,
they enhance code readability and may prevent the introduction of vulnerabilities in the
future.
Version pragmas
● Use consistent version pragmas across all sources, preferably based on the
version used during development (0.5.6 - as specified in truffle-config.js). The
following version pragmas can be observed across the source code:
○ Migrations.sol#1 declares pragma solidity>=0.4.21<0.6.0
○ tokenization/AToken.sol#1 declares pragma solidity^0.5.2
○ All the other sources use pragma solidity^0.5.0.
AToken
● Remove the commented code in this contract. It will increase the readability,
especially during a code review.
CoreLibrary
● Correct the typo in the geNormalizedIncome function name. It will make the
name easier to understand.
LendingPool
● Mark the uint16 _referral parameters in Deposit and Borrow events as
indexed. This will allow external tooling to use bloom filters to quickly determine
inclusion in a given block/transaction.
● In the repay function, move the related newPrincipalBalance logic into the
“borrowRateMode == CoreLibrary.InterestRateMode.FIXED” if block. This will
decrease the gas spent for processing repays with borrowRateMode set to VARIABLE,
as shown in Figure B.1.
function repay(address _reserve, uint256 _amount, address _onBehalfOf) external
payable nonReentrant {
// (...)
if (borrowRateMode == CoreLibrary.InterestRateMode.FIXED) {
// (...) ← move the newPrincipalBalance, its check and
resetUserFixedBorrowRate call here
} else {
// (...)
}
© 2019 Trail of Bits Aave Protocol Assessment | 43
if (newPrincipalBalance == 0 && borrowRateMode ==
CoreLibrary.InterestRateMode.FIXED) {
core.resetUserFixedBorrowRate(_reserve, _onBehalfOf);
}
// (...) - the newPrincipalBalance is not used further
}
© 2019 Trail of Bits Aave Protocol Assessment | 44
C. Property testing using Echidna
Trail of Bits used Echidna, our property-based testing framework, to find logic errors in the
Solidity components of the Aave Protocol.
In this appendix, we present a simple example on how to use Echidna to build a custom
testing harness for specific components of the Aave Protocol. We define a simple invariant
for the FeeProvider and perform a random sequence of smart contract transactions in an
attempt to cause anomalous behavior and break it.
Figure C.1 shows the Solidity source code used to test that the calculation of the fee in
FeeProvider will be always larger than 0. We also restricted the borrow amount used to
calculate the fee to avoid unrealistic scenarios where the value borrowed is 0 or extremely
large. We used the crytic_positiveFee function as an invariant to check that the value
returned by calculateLoanOriginationFee is positive. An example of how to run this test
with Echidna appears in Figure C.2.
pragma solidity ^0.5.0;
import "../fees/FeeProvider.sol";
contract CryticInterface {
address internal crytic_owner = address(0x41414141);
address internal crytic_user = address(0x42424242);
address internal crytic_attacker = address(0x43434343);
}
© 2019 Trail of Bits Aave Protocol Assessment | 45
$ echidna-test . FeeProviderTest --config contracts/crytic/cryticFeeProvider.yaml
...
Analyzing contract: contracts/crytic/cryticFeeProvider.sol:FeeProviderTest
crytic_positiveFee: failed!💥
Call sequence:
setAmount(64)
...
Figure C.2: An example run of Echidna with the cryticFeeProvider.sol test, including test
results.
As a result of running this test, Echidna finds that using small amounts (e.g., 64) could
result in zero-fee loans.
© 2019 Trail of Bits Aave Protocol Assessment | 46
D. Whitepaper Quality Recommendations
The following recommendations are not associated with specific vulnerabilities. However,
they enhance the whitepaper and code readability and may prevent the introduction of
vulnerabilities in the future.
Throughout
● Several variables defined in the whitepaper do not specify if they have a multiplier
(e.g., WAD/RAY or none) while others do. Consider if it would be helpful to do this
for all variables.
1.8 Utilization rate
● This is expressed as a fraction with Lt (total liquidity) in the denominator. The
formula definition does not describe how Lt = 0 should be handled to avoid
attempting division by zero. getReserveUtilizationRate in CoreLibrary.sol
handles this as a special case, returning 0 in the event of no total liquidity.
1.12 Current fixed borrow rate
● The whitepaper refers to an unknown paragraph for details of how Rf should be
calculated. The codebase determines this value in calculateInterestRates in
DefaultReserveInterestRateStrategy.sol by first taking a base value from an oracle,
and then scaling it if utilization is sufficiently high.
1.14 Overall borrow rate
● This is expressed as a fraction with Bt (total borrows) in the denominator. The
formula definition does not describe how Bt = 0 should be handled to avoid
attempting division by zero. getOverallBorrowRateInternal in
DefaultReserveInterestRateStrategy.sol handles this as a special case, returning 0 in
the event of no total borrows.
1.15 Current Liquidity Rate
● Note: “Borrow/Liquidity Rate delta” and “Current Liquidity Rate” are both numbered
1.15
● The whitepaper definition includes subtraction of the Borrow/Liquidity Rate delta.
However, the calculation performed during calls to calculateInterestRates in
DefaultReserveInterestRateStrategy.sol appears to omit this term and only perform
the multiplication.
1.21 Compounded borrow balance
© 2019 Trail of Bits Aave Protocol Assessment | 47
● The Compounded borrow balance denominator cannot be 0, however the code
checks for that because uninitialized ReserveData is read in the
getUserCompoundedBorrowBalance function when a nonexistent reserve is used:
function getUserCompoundedBorrowBalance(address _reserve, address
_user) public view returns (uint256) {
CoreLibrary.UserReserveData storage user =
usersReserveData[_user][_reserve];
CoreLibrary.ReserveData storage reserve =
reserves[_reserve];
return
CoreLibrary.getCompoundedBorrowBalance(
user,
reserve,
blocksPerYear,
getLastBlockNumber()
);
}
Then, the uninitialized data is detected when checking for a zero
principalBorrowBalance:
function getCompoundedBorrowBalance(
CoreLibrary.UserReserveData storage _self,
CoreLibrary.ReserveData storage _reserve,
uint256 _blocksPerYear,
uint256 _currentBlock
) internal view returns (uint256) {
if (_self.principalBorrowBalance == 0) return 0;
…
}
Avoiding the use of uninitialized data will simplify the code, reduce the gas cost and
be easier to review in future audits.
© 2019 Trail of Bits Aave Protocol Assessment | 48
E. Fix Log
From September 23, 2019 to September 25, 2019, Trail of Bits reviewed fixes for issues
identified in this report. The Aave team addressed or accepted the risk of all discovered
issues in their codebase as a result of the assessment. Of those issues, nine were
remediated and six were risk-accepted; we reviewed each of the fixes to understand the
strength of correctness of the proposed remediation. After verifying the fixes for the issues
identified in the report, Trail of Bits also reviewed the new liquidation call functionality and
identified no new security issues.
ID Title Severity Status
05 borrow/repay calls race condition can be exploited to High Fixed
repay more than expected
07 repay does not validate if the user borrows ERC20 Low Fixed
tokens, but pays using ether
08 redeem does not properly validate parameters and High Fixed
allows drainage of funds
11 There is no way to redeem all available tokens when Medium Risk
Lending Pool parameters are close to the redemption Accepted
limit
© 2019 Trail of Bits Aave Protocol Assessment | 49
12 Race condition on LendingPool allows attacker to Medium Risk
redeem their token first Accepted
14 Users should check that test mode is disabled before Informational Fixed
interacting with the Lending Pool
© 2019 Trail of Bits Aave Protocol Assessment | 50
Detailed Fix Log
Finding 1: Solidity compiler optimizations can be dangerous
Risk accepted. Aave indicated that compiler optimizations provide substantial gas savings
to users and these savings currently outweigh the risk of optimizer bugs.
https://gitlab.com/aave-tech/dlp/contracts/issues/1
Finding 2: Lack of access controls in updateReserveTotalBorrowsByRateMode
Fixed. Aave added the missing onlyLendingPool modifier to the affected function.
https://gitlab.com/aave-tech/dlp/contracts/commit/1ee87735b657229f1d57887fbc8fa4ce8e
615156
Finding 3: Lack of access control for LendingPoolAddressesProvider and
NetworkMetadataProvider
Risk accepted. The Aave team acknowledged this issue and plans to use DAOstack to add
the missing access controls before release. However, as this governance system was not in
place at the time of this assessment or retest, the issue remains open.
https://gitlab.com/aave-tech/dlp/contracts/issues/3
Finding 4: Lack of access control for FeeProvider setter functions
Fixed. The Aave team added the suggested onlyOwner modifiers to the affected functions.
https://gitlab.com/aave-tech/dlp/contracts/merge_requests/6
Finding 5: borrow/repay calls race condition can be exploited to repay more than
expected
Fixed. The Aave team added a check to require users repaying on behalf of another user to
specify the amount to repay instead of using the UINT_MAX shortcut.
https://gitlab.com/aave-tech/dlp/contracts/merge_requests/21
Finding 6: repay calls can be blocked by front-running them with another repay call
Risk accepted. The Aave team indicated that they will provide documentation about this
issue and offer support to affected users.
https://gitlab.com/aave-tech/dlp/contracts/issues/6
Finding 7: repay does not validate if the user borrowed ERC20 tokens, but pays using
ether
Fixed. The Aave team added a check to ensure that no ETH is being sent when repaying an
ERC20 loan.
https://gitlab.com/aave-tech/dlp/contracts/merge_requests/15
Finding 8: redeem does not properly validate parameters and allows drainage of funds
© 2019 Trail of Bits Aave Protocol Assessment | 51
Fixed. The Aave team restructured the affected function to move some of the logic to the
AToken contract and restrict access to the companion function in the LendingPool to the
AToken itself.
https://gitlab.com/aave-tech/dlp/contracts/merge_requests/7
Finding 9: updateReserveTotalBorrowsByRateMode does not properly validate
InterestRateMode inputs
Fixed. The Aave team added a require statement that checks that the input
InterestRateMode is within the correct range.
https://gitlab.com/aave-tech/dlp/contracts/merge_requests/16
Finding 10: DefaultReserveInterestRateStrategy parameters are not validated and
can block the Lending Pool
Risk accepted. The Aave team indicated they plan to manually validate new
InterestRateStrategy contract parameters after they are deployed, but before enabling
support for them.
https://gitlab.com/aave-tech/dlp/contracts/issues/15
Finding 11: There is no way to redeem all available tokens when Lending Pool
parameters are close to the redemption limit
Risk accepted. The Aave team indicated that this is expected behavior and that they will
provide documentation to that effect to users.
https://gitlab.com/aave-tech/dlp/contracts/issues/16
Finding 12: Race condition on LendingPool allows attacker to redeem their token first
Risk accepted. The Aave team has indicated that they will document this risk for users and
assess mitigations for this type of issue going forward.
https://gitlab.com/aave-tech/dlp/contracts/issues/17
Finding 13: Insufficient validation of reserve address in some LendingPoolCore
functions
Fixed. The Aave team added a new modifier, onlyActiveReserve, to ensure functions are
only executed against reserve data that has been initialized. For gas usage reasons, this
modifier was not added to the functions highlighted in the finding description; however, all
paths to these functions are otherwise covered by this modifier.
https://gitlab.com/aave-tech/dlp/contracts/issues/18
Finding 14: Users should check that test mode is disabled before interacting with the
Lending Pool
Fixed. The Aave team removed the test mode functionality from the codebase.
https://gitlab.com/aave-tech/dlp/contracts/merge_requests/13/
© 2019 Trail of Bits Aave Protocol Assessment | 52
Finding 15: swapBorrowRateMode does not check if reserve allows fixed interest
rates
Fixed. The Aave team added a check to ensure the reserve allows fixed borrowing rates
when a user attempts to swap their interest rate mode.
https://gitlab.com/aave-tech/dlp/contracts/merge_requests/5
© 2019 Trail of Bits Aave Protocol Assessment | 53
Detailed Issue Discussion
Responses from Aave for risk-accepted and unfixed issues are included as quotes below.
Finding 1: Solidity compiler optimizations can be dangerous
We believe that right now, the gas cost savings for the users (which we estimated
in roughly 30-40%) outweigh the risk of possible unknown bugs. We will continue
monitoring the evolution of Solidity compilers to ensure there are no security
concerns.
Finding 6: repay calls can be blocked by front-running them with another repay call
We will make sure users understand this behavior, and provide support in case of
attack. No changes will be applied to the protocol.
Finding 10: DefaultReserveInterestRateStrategy parameters are not validated and
can block the Lending Pool
The way this will work, InterestRateStrategy contracts will be changed upon
governance vote in case specific conditions arise (e.g., sudden market changes). In
this case, new InterestRateStrategy contracts will be preemptively audited
and deployed, and made public before the governance vote. In case an incorrect
version is deployed, the reserve can be paused and a new
InterestRateStrategy contract can be deployed that fixes the issue.
Finding 11: There is no way to redeem all available tokens when Lending Pool
parameters are close to the redemption limit
We believe it is a behavior that is intrinsic to the protocol specification. Even
real-life banking entities might incur in it in case of a bank run, so it's not
dependent on the specific implementation. We also provide tokenization functions
to allow users, to a certain degree, to use aTokens instead of the underlying asset
if necessity arises. We will make sure to properly document this behavior.
Finding 12: Race condition on LendingPool allows attacker to redeem their token first
We believe that, given the transactional nature of the ethereum blockchain, how
the current transaction execution is handled, and how the protocol works, there is
no effective solution to this. We will make sure to inform users about this
behavior and keep following innovations at the blockchain level that might help
alleviate this issue in the future.
© 2019 Trail of Bits Aave Protocol Assessment | 54