Liquidations
Aave 协议的健康状态取决于系统内的贷款的“健康状态(helath)”,也称为“健康因子(health factor)”。当账户总贷款的“health factor”低于 1 时,任何人都可以对 LendingPool
合约进行 liquidationCall
,偿还部分欠款并获得部分抵押品作为回报(也成为此处列出的清算奖金)
这激励第三方通过以自身利益行事(获得部分抵押品)来参与整个协议的健康,从而确保贷款有足够的抵押品。
参与清算的方式有多种:
- 通过在
LendingPool
合约中直接调用liquidationCall()
- 通过创建自己的自动化机器人或系统来清算贷款
要使 liquidation call 有利可图,我们必须考虑清算贷款所设计的 gas 成本。若使用高 gas 价格,那么清算对于我们来说可能无利可图。
先决条件
在进行 liquidationCall()
时,必须:
- 知道 health factor 低于 1 的用户(即以太坊地址:
user
) 知道有效债务金额(
debtToCover
)和可偿还的债务资产(debt
)- 平仓因子为 0.5,这意味着每个有效的
liquidationCall()
最多只能清算 50% 的债务 。 - 如此所述,我们可以将
debtToCover
设置为uint(-1)
,协议将以平仓因子允许的最高清算方式进行 - 我们必须已经有了足够的债务资产金额,
liquidationCall()
将使用它来偿还债务
- 平仓因子为 0.5,这意味着每个有效的
了解我们要关闭的抵押资产(
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
对象。
- 我们需要传递到 Aave.js 方法的数据可以从我们的子图中获取,即
执行 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 成本
计算盈利能力的一种方法如下:
- 存储和检索每个抵押品的相关详细信息,例如地址、精度和清算奖金
- 获得用户的抵押余额(
aTokenBalance
) - 根据 Aave 的预言机合约(
getAssetPrice()
)获得资产的价格 - 我们可以获得的最大抵押品赏金将是抵押品金额 (2) 乘以清算奖金 (1) 乘以抵押品资产的 ETH 价格 (3)。注意:对于 USDC 等资产,小数位数与其他资产不同
- 我们的交易的最大成本将是我们的 Gas 价格乘以所使用的 gas 数量。我们应该通过 web3 提供商调用
estimateGas
来更高的估计所使用的 gas 量。 - 我们近似利润将是抵押奖金 (4) 减去我们的交易成本 (5) 的价值
Appendix (附录)
健康因子是如何计算的?
健康因子的计算公式为:用户的抵押金额(以 ETH 为单位)乘以用户所未偿还资产的当前清算阈值,再除以 100,再除以用户的借款余额和费用(以 ETH 为单位),
这即可以在链下计算,也可以在链上计算,分别参见 Aave.js
和 GenericLogic
库合约。
清算赏金是如何计算的?
目前,清算赏金是否风险团队根据流动性风险进行评估和确定,并在此处更新
这种情况将在未来通过 Aave 治理协议而改变。
Price oracles
Aave Protocol 使用 Chainlink 作为价格预言机,并在 Chainlink 发生故障时提供备份预言机。更多详细信息,参阅 价格预言机 部分。
账户的运行状况因素有用户的账户数据和相关资产的价格决定,上次由 Price Oracle 更新。