Liquidations

The health of the Aave Protocol is dependant on the 'health' of the loans within the system, also known as the 'health factor'. When the 'health factor' of an account's total loans is below 1, anyone can make a liquidationCall() to the LendingPool contract, paying back part of the debt owed and receiving discounted collateral in return (also known as the liquidation bonus as listed here).

This incentivises third parties to participate in the health of the overall protocol, by acting in their own interest (to receive the discounted collateral) and as a result, ensure loans are sufficiently collateralised.

There are multiple ways to participate in liquidations:

  1. By using the Liquidations module in the Aave app: https://app.aave.com/liquidations.

  2. By calling the liquidationCall() directly in the LendingPool contract.

  3. By creating your own automated bot or system to liquidate loans.

This section will focus on (2) and (3).

For liquidation calls to be profitable, you must take into account the gas cost involved in liquidating the loan. If a high gas price is used, then the liquidation may be unprofitable for you. See the 'Calculating profitability vs gas cost' section for more details.

0. Prerequisites

When making a liquidationCall(), you must:

  • Know the account (i.e. the ethereum address: _user) whose health factor is below 1.

  • Know the valid debt amount (_purchaseAmount) and debt asset (_reserve) that can be paid.

    • The close factor is 0.5, which means that only a maximum of 50% of the debt can be liquidated per valid liquidationCall().

    • As mentioned here, you can set the _purchaseAmount to uint(-1) and the protocol will proceed with the highest possible liquidation allowed by the close factor.

    • You must already have a sufficient balance of the debt asset, which will be used by the liquidationCall() to pay back the debts.

  • Know the collateral asset (_collateral) you are closing. I.e. the collateral asset that the user has 'backing' their outstanding loan that you will partly receive as a 'bonus'.

  • Whether you want to receive aTokens or the underlying asset (_receiveaToken) after a successful liquidationCall().

1. Getting accounts to liquidate

Only user accounts that have a health factor below 1 can be liquidated. There are multiple ways you can get the health factor, with most of them involving 'user account data'.

"Users" in the Aave Protocol refer to a single ethereum address that has interacted with the protocol. This can be an externally owned account or contract.

On-chain

  1. To gather user account data from on-chain data, one way would be to monitor emitted events from the protocol and keep an up to date index of user data locally.

    1. Events are emitted each time a user interacts with the protocol (deposit, repay, borrow, etc). A list of these events can be found in the Emitted Events section.

  2. When you have the user's address, you can simply call getUserAccountData() to read the user's current healthFactor. If the healthFactor is below 1, then the account can be liquidated.

Our API

We have a limited API that powers app.aave.com/liquidations, that is also publicly available. Note that there is a small delay on the API due to Redis caching to prevent server overload. The server data is fetched every 20 seconds.

  1. To access this API, send a GET request to https://protocol-api.aave.com/liquidations?get=proto for the main Aave market, or https://protocol-api.aave.com/liquidations?get=uniswap for the Uniswap market.

  2. Data for all the user accounts that can be liquidated (i.e. health factor < 1) will be returned as JSON. An example is below:

{
"data": [
{
"principalBorrows": "0.004465184096266527",
"currentBorrows": "0.004490492698072727",
"currentBorrowsETH": "0.004490492698072727",
"currentBorrowsUSD": "1.7751815734",
"reserve": {
"id": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0x24a42fd28c976a61df5d00d0599c34c4f90748c8",
"underlyingAsset": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"symbol": "ETH",
"decimals": 18,
"__typename": "Reserve"
},
"user": {
"id": "0x0086ff1cf0f7fe2bbd8e67d88da5c4e8fb528012",
"reservesData": [
{
"principalATokenBalance": "0.005609491180194647",
"userBalanceIndex": "1.00018336137043934501",
"redirectedBalance": "0",
"interestRedirectionAddress": "0x0000000000000000000000000000000000000000",
"usageAsCollateralEnabledOnUser": true,
"borrowRate": "0.02137205634101432528",
"borrowRateMode": "Variable",
"originationFee": "0.000011162960240666",
"principalBorrows": "0.004465184096266527",
"variableBorrowIndex": "1.00088169838070560383",
"lastUpdateTimestamp": 1581425534,
"__typename": "UserReserve",
"principalBorrowsUSD": "1.7651765769",
"currentBorrowsUSD": "1.7751815734",
"originationFeeUSD": "0.0044129414",
"currentUnderlyingBalanceUSD": "2.2213099798",
"originationFeeETH": "0.000011162960240666",
"currentBorrows": "0.004490492698072727",
"currentBorrowsETH": "0.004490492698072727",
"principalBorrowsETH": "0.004465184096266527",
"currentUnderlyingBalance": "0.005619017453838833",
"currentUnderlyingBalanceETH": "0.005619017453838833",
"reserve": {
"id": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0x24a42fd28c976a61df5d00d0599c34c4f90748c8",
"underlyingAsset": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Ethereum",
"symbol": "ETH",
"decimals": 18,
"liquidityRate": "0.00009370207161082542",
"usageAsCollateralEnabled": true,
"reserveLiquidationBonus": "0.05",
"lastUpdateTimestamp": 1598356348,
"aToken": {
"id": "0x3a3a65aab0dd2a17e3f1947ba16138cd37d08c04",
"__typename": "AToken"
},
"__typename": "Reserve"
}
}
],
"totalLiquidityETH": "0.005619017453838833",
"totalLiquidityUSD": "2.2213099798",
"totalCollateralETH": "0.005619017453838833",
"totalCollateralUSD": "2.2213099798",
"totalFeesETH": "0.000011162960240666",
"totalFeesUSD": "0.0044129414",
"totalBorrowsETH": "0.004490492698072727",
"totalBorrowsUSD": "1.7751815734",
"totalBorrowsWithFeesETH": "0.004501655658313393",
"totalBorrowsWithFeesUSD": "1.7795945148",
"availableBorrowsETH": "0",
"currentLoanToValue": "0.75",
"currentLiquidationThreshold": "0.8",
"maxAmountToWithdrawInEth": "-0.000008052119052908",
"healthFactor": "0.99856903865349397483"
},
"id": "0x0086ff1cf0f7fe2bbd8e67d88da5c4e8fb5280120xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0x24a42fd28c976a61df5d00d0599c34c4f90748c8"
},
],
"timestamp": 1598357466107
}

GraphQL

  1. Similarly to the sections above, you will need to gather user account data and keep an index of the user data locally.

  2. Since GraphQL does not provide real time calculated user data such as the healthFactor, you will need to compute this yourself. The easiest way is to use the Aave.js package, which has methods to compute summary user data.

    1. The data you will need to pass into the Aave.js method's can be fetched from our subgraph, namely the UserReserve objects.

2. Executing the liquidation call

Once you have the account(s) to liquidate, you will need to calculate the amount of collateral that can be liquidated:

  1. Use getUserReserveData() (for Solidity) or the UserReserve object (for GraphQL) with the relevant parameters.

  2. For reserves that have usageAsCollateralEnabled as true, the currentATokenBalance multiplied by the current close factor is the amount that can be liquidated.

    1. For example, if the current close factor is 0.5 and the aToken balance is 1e18 tokens, then the maximum amount that can be liquidated is 5e17.

    2. You can also pass in uint(-1) as the _purchaseAmount in liquidationCall() to liquidate the maximum amount allowed.

In Aave V1, flash loans within the protocol are not available due to reentrancy protections, therefore cannot be used in liquidation calls.

Solidity

Below is an example contract. When making the liquidationCall() to the LendingPool contract, your contract must already have at least _purchaseAmount of _reserve.

Liquidator.sol
ILendingPoolAddressesProvider.sol
ILendingPool.sol
Liquidator.sol
pragma solidity ^0.6.6;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./ILendingPoolAddressesProvider.sol";
import "./ILendingPool.sol";
contract Liquidator {
address constant aaveEthAddress = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address constant lendingPoolAddressProvider = 0x24a42fD28C976A61Df5D00D0599C34c4f90748c8; // Aave market - mainnet
function myLiquidationFunction(
address _collateral,
address _reserve,
address _user,
uint256 _purchaseAmount,
bool _receiveaToken
)
external
{
ILendingPoolAddressesProvider addressProvider = ILendingPoolAddressesProvider(lendingPoolAddressProvider);
if (_reserve != aaveEthAddress) {
require(IERC20(_reserve).approve(addressProvider.getLendingPoolCore(), _purchaseAmount), "Approval error");
}
ILendingPool lendingPool = ILendingPool(addressProvider.getLendingPool());
// Assumes this contract already has `_purchaseAmount` of `_reserve`.
lendingPool.liquidationCall{value: _reserve == aaveEthAddress ? _purchaseAmount : 0}(_collateral, _reserve, _user, _purchaseAmount, _receiveaToken);
}
}
ILendingPoolAddressesProvider.sol
pragma solidity ^0.6.6;
interface ILendingPoolAddressesProvider {
function getLendingPoolCore() external view returns (address payable);
function getLendingPool() external view returns (address);
}
ILendingPool.sol
pragma solidity ^0.6.6;
interface ILendingPool {
function liquidationCall ( address _collateral, address _reserve, address _user, uint256 _purchaseAmount, bool _receiveAToken ) external payable;
}

Javascript

A similar call can be made with a package such as Web3.js. The account making the call must already have at least the _purchaseAmount of _reserve.

// Import the ABIs, see: https://docs.aave.com/developers/developing-on-aave/deployed-contract-instances
import DaiTokenABI from "./DAItoken.json"
import LendingPoolAddressesProviderABI from "./LendingPoolAddressesProvider.json"
import LendingPoolABI from "./LendingPool.json"
// ... The rest of your code ...
// Input variables
const collateralAddress = 'THE_COLLATERAL_ASSET_ADDRESS'
const daiAmountInWei = web3.utils.toWei("1000", "ether").toString()
const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // mainnet DAI
const user = 'USER_ACCOUNT'
const receiveATokens = true
const lpAddressProviderAddress = '0x24a42fD28C976A61Df5D00D0599C34c4f90748c8' // mainnet
const lpAddressProviderContract = new web3.eth.Contract(LendingPoolAddressesProviderABI, lpAddressProviderAddress)
// Get the latest LendingPoolCore address
const lpCoreAddress = await lpAddressProviderContract.methods
.getLendingPoolCore()
.call()
.catch((e) => {
throw Error(`Error getting lendingPool address: ${e.message}`)
})
// Approve the LendingPoolCore address with the DAI contract
const daiContract = new web3.eth.Contract(DAITokenABI, daiAddress)
await daiContract.methods
.approve(
lpCoreAddress,
daiAmountInWei
)
.send()
.catch((e) => {
throw Error(`Error approving DAI allowance: ${e.message}`)
})
// Get the latest LendingPool contract address
const lpAddress = await lpAddressProviderContract.methods
.getLendingPool()
.call()
.catch((e) => {
throw Error(`Error getting lendingPool address: ${e.message}`)
})
// Make the deposit transaction via LendingPool contract
const lpContract = new web3.eth.Contract(LendingPoolABI, lpAddress)
await lpContract.methods
.liquidationCall(
collateralAddress,
daiAddress,
user,
daiAmountInWei,
receiveATokens,
)
.send()
.catch((e) => {
throw Error(`Error liquidating user with error: ${e.message}`)
})

3. Setting up a bot

Depending on your environment, preferred programming tools and languages, your bot should:

  • Ensure it has enough (or access to enough) funds when liquidating.

  • Calculate the profitability of liquidating loans vs gas costs, taking into account the most lucrative collateral to liquidate.

  • Ensure it has access to the latest protocol user data.

  • Have the usual fail safes and security you'd expect for any production service.

Calculating profitability vs gas cost

One way to calculate the profitability is the following:

  1. Store and retrieve each collateral's relevant details such as address, decimals used, and liquidation bonus as listed here.

  2. Get the user's collateral balance (aTokenBalance).

  3. Get the asset's price according to the Aave's oracle contract (getAssetPrice()).

  4. The maximum collateral bonus you can receive will be the collateral balance (2) multiplied by the liquidation bonus (1) multiplied by the collateral asset's price in ETH (3). Note that for assets such as USDC, the number of decimals are different from other assets.

  5. The maximum cost of your transaction will be your gas price multiplied by the amount of gas used. You should be able to get a good estimation of the gas amount used by calling estimateGas via your web3 provider.

  6. Your approximate profit will be the value of the collateral bonus (4) minus the cost of your transaction (5).

Appendix

How is health factor calculated?

The health factor is calculated from: the user's collateral balance (in ETH) multiplied by the current liquidation threshold for all the user's outstanding assets, divided by 100, divided by the user's borrow balance and fees (in ETH). More info here.

This can be both calculated off-chain and on-chain, see Aave.js and the LendingPoolDataProvider contract, respectively.

How is liquidation bonus determined?

At the moment, liquidation bonuses are evaluated and determined by the risk team based on liquidity risk and updated here.

This will change in the future with Aave Protocol Governance.

Price oracles

Aave Protocol uses Chainlink as a price oracle, with a backup oracle in case of a Chainlink malfunction. See our Price Oracle section for more details.

The health factor of accounts is determined by the user's account data and the price of relevant assets, as last updated by the Price Oracle.