Aave 协议的健康状态取决于系统内的贷款的“健康状态(helath)”,也称为“健康因子(health factor)”。当账户总贷款的“health factor”低于 1 时,任何人都可以对 LendingPool合约进行 liquidationCall,偿还部分欠款并获得部分抵押品作为回报(也成为此处列出的清算奖金)

这激励第三方通过以自身利益行事(获得部分抵押品)来参与整个协议的健康,从而确保贷款有足够的抵押品。

参与清算的方式有多种:

  1. 通过在 LendingPool合约中直接调用 liquidationCall()
  2. 通过创建自己的自动化机器人或系统来清算贷款

要使 liquidation call 有利可图,我们必须考虑清算贷款所设计的 gas 成本。若使用高 gas 价格,那么清算对于我们来说可能无利可图。

先决条件

在进行 liquidationCall()时,必须:

  • 知道 health factor 低于 1 的用户(即以太坊地址:user
  • 知道有效债务金额(debtToCover)和可偿还的债务资产(debt

    • 平仓因子为 0.5,这意味着每个有效的 liquidationCall()最多只能清算 50% 的债务 。
    • 如此所述,我们可以将 debtToCover设置为 uint(-1),协议将以平仓因子允许的最高清算方式进行
    • 我们必须已经有了足够的债务资产金额,liquidationCall()将使用它来偿还债务
  • 了解我们要关闭的抵押资产(collateral)。即用户“支持”其未偿还贷款的抵押资产,我们将获得部分的抵押资产作为我们的清算奖金

  • 我们是否想在成功 liquidationCall() 后接收 aToken 或者底层资产(receiverAToken

获取账户以进行清算

只有健康因子低于 1 的用户的账户才能被清算。有很多方法可用查看健康因子。其中大多数都涉及“用户账户数据”。

Aave 协议中的“用户”是指与协议交互的单个以太坊地址。这可以是 EOA 账户(external owned account)或合约账户

On-chain

  • 要从链上数据中收集用户账户数据,一种方法是监控协议触发的事件,并在本地保持用户数据的最新索引。

    • 每次用户与协议交互(存款,还款,借款等)是都会触发事件。有关相关事件,请参阅合约源代码
  • 当我们拥有用户的地址时,我们只需调用 getUserAccountData()来读取用户当前的 healthFactor。若 healthFactor低于 1,则可以清算该账户。

GraphQL

  • 与上述部分相似,如果需要收集用户账户数据并在本地保留用户数据的 index
  • 由于 GraphQL 不提供实时计算的用户数据,例如 healthFactor,因此您需要自己计算这些数据。最简单的方法是使用 Aave.js 包,其中包含计算摘要用户数据的方法。

    • 我们需要传递到 Aave.js 方法的数据可以从我们的子图中获取,即 UserReserve 对象。

执行 liquidation call

一旦确定了要清算的账户,我们需要计算可以清算的抵押品数量

  • 在 Protocol Data Provider 合约(对于 Solidity)或 UserReserve 对象(对于 GraphQL )使用 getUserReserveData()并提供相关参数
  • 单次清算可清算的最大债务由当前的平仓因子决定(当前为 0.5)
    debtToCover = (userStableDebt + userVariableDebt) * LiquidationCloseFactorPercent

    • 我们还可以在 liquidationCall()中传入 type(uint).max作为 debtToCover来清算允许的最大金额
  • 对于将 usageAsCollateralEnable设置为true的储备,当前的清算奖金决定了为清算债务所需的最大抵押品数量:
    maxAmountOfCollateralToLiquidate = (debtAeestPrice * debtToCover * liquidationBonus) / collateralPrice

solidity

下面是一个合约示例。当对 LendingPool合约进行 liquidationCall()时,我们的合约必须至少已经有 debtToCover的债务

// Liquidator.sol
pragma solidity ^0.6.6;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./ILendingPoolAddressesProvider.sol";
import "./ILendingPool.sol";


contract Liquidator {

    address constant lendingPoolAddressProvider = INSERT_LENDING_POOL_ADDRESS

    function myLiquidationFunction(
        address _collateral, 
        address _reserve,
        address _user,
        uint256 _purchaseAmount,
        bool _receiveaToken
    )
        external
    {
        ILendingPoolAddressesProvider addressProvider = ILendingPoolAddressesProvider(lendingPoolAddressProvider);
  
        ILendingPool lendingPool = ILendingPool(addressProvider.getLendingPool());
        
        require(IERC20(_reserve).approve(address(lendingPool), _purchaseAmount), "Approval error");

        // Assumes this contract already has `_purchaseAmount` of `_reserve`.
        lendingPool.liquidationCall(_collateral, _reserve, _user, _purchaseAmount, _receiveaToken);
    }
}
// ILendingPoolAddressesProvider.sol
pragma solidity ^0.6.6;

interface ILendingPoolAddressesProvider {
    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/Python

我们可以使用 Web3.js/Web3.py 等包进行类似的调用。发出调用的账户必须至少已经拥有 debeToCover数量的debt

// 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 = '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5' // mainnet
const lpAddressProviderContract = new web3.eth.Contract(LendingPoolAddressesProviderABI, lpAddressProviderAddress)

// Get the latest LendingPool contract address
const lpAddress = await lpAddressProviderContract.methods
    .getLendingPool()
    .call()
    .catch((e) => {
        throw Error(`Error getting lendingPool address: ${e.message}`)
    })

// Approve the LendingPool address with the DAI contract
const daiContract = new web3.eth.Contract(DAITokenABI, daiAddress)
await daiContract.methods
    .approve(
        lpAddress,
        daiAmountInWei
    )
    .send()
    .catch((e) => {
        throw Error(`Error approving DAI allowance: ${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}`)
    })
from web3 import Web3
import json

w3 = Web3(Web3.HTTPProvider(PROVIDER_URL))

def loadAbi(abi):
  return json.load(open("./abis/%s"%(abi)))
  
def getContractInstance(address, abiFile):
  return w3.eth.contract(address, abi=loadAbi(abiFile))
  
def liquidate(user, liquidator, amount):

  allowance = dai.functions.allowance(user, lendingPool.address).call()

  # Approve lendingPool to spend liquidator's funds
  if allowance <= 0:
    tx = dai.functions.approve(lendingPool.address, amount).transact({
      "from": liquidator,
    })
 
  # Liquidation Call, collateral: weth and debt: dai
  lendingPool.functions.liquidationCall(
    weth.address,
    dai.address,
    user,
    amount,
    True
  ).transact({"from": liquidator})
  
dai = getContractInstance("0x6B175474E89094C44Da98b954EedeAC495271d0F", "DAI.json")
weth = getContractInstance("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "WETH.json")
lendingPoolAddressProvider = getContractInstance("0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", "LENDING_POOL_PROVIDER.json")
lendingPool = getContractInstance(
    # Get address of latest lendingPool from lendingPoolAddressProvider
    lendingPoolAddressProvider.functions.getLendingPool().call(),
    "LENDING_POOL.json"
  )

liquidate(alice, bob, amount)

Setting up a bot

根据我们的环境、首选编程工具和语言,我们的机器人应该:

  • 确保他在清算时有足够的(或获得足够的)资金
  • 计算 liquidation loans 的盈利情况与 gas 成本,同时考虑最有利可图的清算抵押品
  • 确保他有权访问最新的协议用户数据
  • 拥有我们预期用于任何生产服务的常见故障保险和安全性

计算盈利能力与 gas 成本

计算盈利能力的一种方法如下:

  1. 存储和检索每个抵押品的相关详细信息,例如地址、精度和清算奖金
  2. 获得用户的抵押余额(aTokenBalance
  3. 根据 Aave 的预言机合约(getAssetPrice())获得资产的价格
  4. 我们可以获得的最大抵押品赏金将是抵押品金额 (2) 乘以清算奖金 (1) 乘以抵押品资产的 ETH 价格 (3)。注意:对于 USDC 等资产,小数位数与其他资产不同
  5. 我们的交易的最大成本将是我们的 Gas 价格乘以所使用的 gas 数量。我们应该通过 web3 提供商调用 estimateGas来更高的估计所使用的 gas 量。
  6. 我们近似利润将是抵押奖金 (4) 减去我们的交易成本 (5) 的价值

Appendix (附录)

健康因子是如何计算的?

健康因子的计算公式为:用户的抵押金额(以 ETH 为单位)乘以用户所未偿还资产的当前清算阈值,再除以 100,再除以用户的借款余额和费用(以 ETH 为单位),

这即可以在链下计算,也可以在链上计算,分别参见 Aave.jsGenericLogic库合约。

清算赏金是如何计算的?

目前,清算赏金是否风险团队根据流动性风险进行评估和确定,并在此处更新

这种情况将在未来通过 Aave 治理协议而改变。

Price oracles

Aave Protocol 使用 Chainlink 作为价格预言机,并在 Chainlink 发生故障时提供备份预言机。更多详细信息,参阅 价格预言机 部分。

账户的运行状况因素有用户的账户数据和相关资产的价格决定,上次由 Price Oracle 更新。