aave-protocol-v2/contracts/lendingpool/LendingPoolLiquidationManager.sol

361 lines
14 KiB
Solidity

// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.6.8;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../libraries/openzeppelin-upgradeability/VersionedInitializable.sol";
import "../configuration/LendingPoolAddressesProvider.sol";
import "../configuration/LendingPoolParametersProvider.sol";
import "../tokenization/AToken.sol";
import "../libraries/CoreLibrary.sol";
import "../libraries/WadRayMath.sol";
import "./LendingPoolCore.sol";
import "./LendingPoolDataProvider.sol";
import "../interfaces/IPriceOracleGetter.sol";
/**
* @title LendingPoolLiquidationManager contract
* @author Aave
* @notice Implements the liquidation function.
**/
contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializable {
using SafeMath for uint256;
using WadRayMath for uint256;
using Address for address;
LendingPoolAddressesProvider public addressesProvider;
LendingPoolCore core;
LendingPoolDataProvider dataProvider;
LendingPoolParametersProvider parametersProvider;
IFeeProvider feeProvider;
address ethereumAddress;
uint256 constant LIQUIDATION_CLOSE_FACTOR_PERCENT = 50;
/**
* @dev emitted when a borrow fee is liquidated
* @param _collateral the address of the collateral being liquidated
* @param _reserve the address of the reserve
* @param _user the address of the user being liquidated
* @param _feeLiquidated the total fee liquidated
* @param _liquidatedCollateralForFee the amount of collateral received by the protocol in exchange for the fee
* @param _timestamp the timestamp of the action
**/
event OriginationFeeLiquidated(
address indexed _collateral,
address indexed _reserve,
address indexed _user,
uint256 _feeLiquidated,
uint256 _liquidatedCollateralForFee,
uint256 _timestamp
);
/**
* @dev emitted when a borrower is liquidated
* @param _collateral the address of the collateral being liquidated
* @param _reserve the address of the reserve
* @param _user the address of the user being liquidated
* @param _purchaseAmount the total amount liquidated
* @param _liquidatedCollateralAmount the amount of collateral being liquidated
* @param _accruedBorrowInterest the amount of interest accrued by the borrower since the last action
* @param _liquidator the address of the liquidator
* @param _receiveAToken true if the liquidator wants to receive aTokens, false otherwise
* @param _timestamp the timestamp of the action
**/
event LiquidationCall(
address indexed _collateral,
address indexed _reserve,
address indexed _user,
uint256 _purchaseAmount,
uint256 _liquidatedCollateralAmount,
uint256 _accruedBorrowInterest,
address _liquidator,
bool _receiveAToken,
uint256 _timestamp
);
enum LiquidationErrors {
NO_ERROR,
NO_COLLATERAL_AVAILABLE,
COLLATERAL_CANNOT_BE_LIQUIDATED,
CURRRENCY_NOT_BORROWED,
HEALTH_FACTOR_ABOVE_THRESHOLD,
NOT_ENOUGH_LIQUIDITY
}
struct LiquidationCallLocalVars {
uint256 userCollateralBalance;
uint256 userCompoundedBorrowBalance;
uint256 borrowBalanceIncrease;
uint256 maxPrincipalAmountToLiquidate;
uint256 actualAmountToLiquidate;
uint256 liquidationRatio;
uint256 maxAmountCollateralToLiquidate;
uint256 originationFee;
uint256 feeLiquidated;
uint256 liquidatedCollateralForFee;
CoreLibrary.InterestRateMode borrowRateMode;
uint256 userStableRate;
bool isCollateralEnabled;
bool healthFactorBelowThreshold;
}
/**
* @dev as the contract extends the VersionedInitializable contract to match the state
* of the LendingPool contract, the getRevision() function is needed.
*/
function getRevision() internal override pure returns (uint256) {
return 0;
}
/**
* @dev users can invoke this function to liquidate an undercollateralized position.
* @param _reserve the address of the collateral to liquidated
* @param _reserve the address of the principal reserve
* @param _user the address of the borrower
* @param _purchaseAmount the amount of principal that the liquidator wants to repay
* @param _receiveAToken true if the liquidators wants to receive the aTokens, false if
* he wants to receive the underlying asset directly
**/
function liquidationCall(
address _collateral,
address _reserve,
address _user,
uint256 _purchaseAmount,
bool _receiveAToken
) external payable returns (uint256, string memory) {
// Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables
LiquidationCallLocalVars memory vars;
(, , , , , , , vars.healthFactorBelowThreshold) = dataProvider.calculateUserGlobalData(
_user
);
if (!vars.healthFactorBelowThreshold) {
return (
uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD),
"Health factor is not below the threshold"
);
}
vars.userCollateralBalance = core.getUserUnderlyingAssetBalance(_collateral, _user);
//if _user hasn't deposited this specific collateral, nothing can be liquidated
if (vars.userCollateralBalance == 0) {
return (
uint256(LiquidationErrors.NO_COLLATERAL_AVAILABLE),
"Invalid collateral to liquidate"
);
}
vars.isCollateralEnabled =
core.isReserveUsageAsCollateralEnabled(_collateral) &&
core.isUserUseReserveAsCollateralEnabled(_collateral, _user);
//if _collateral isn't enabled as collateral by _user, it cannot be liquidated
if (!vars.isCollateralEnabled) {
return (
uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED),
"The collateral chosen cannot be liquidated"
);
}
//if the user hasn't borrowed the specific currency defined by _reserve, it cannot be liquidated
(, vars.userCompoundedBorrowBalance, vars.borrowBalanceIncrease) = core
.getUserBorrowBalances(_reserve, _user);
if (vars.userCompoundedBorrowBalance == 0) {
return (
uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED),
"User did not borrow the specified currency"
);
}
//all clear - calculate the max principal amount that can be liquidated
vars.maxPrincipalAmountToLiquidate = vars
.userCompoundedBorrowBalance
.mul(LIQUIDATION_CLOSE_FACTOR_PERCENT)
.div(100);
vars.actualAmountToLiquidate = _purchaseAmount > vars.maxPrincipalAmountToLiquidate
? vars.maxPrincipalAmountToLiquidate
: _purchaseAmount;
(uint256 maxCollateralToLiquidate, uint256 principalAmountNeeded) = calculateAvailableCollateralToLiquidate(
_collateral,
_reserve,
vars.actualAmountToLiquidate,
vars.userCollateralBalance
);
vars.originationFee = core.getUserOriginationFee(_reserve, _user);
//if there is a fee to liquidate, calculate the maximum amount of fee that can be liquidated
if (vars.originationFee > 0) {
(
vars.liquidatedCollateralForFee,
vars.feeLiquidated
) = calculateAvailableCollateralToLiquidate(
_collateral,
_reserve,
vars.originationFee,
vars.userCollateralBalance.sub(maxCollateralToLiquidate)
);
}
//if principalAmountNeeded < vars.ActualAmountToLiquidate, there isn't enough
//of _collateral to cover the actual amount that is being liquidated, hence we liquidate
//a smaller amount
if (principalAmountNeeded < vars.actualAmountToLiquidate) {
vars.actualAmountToLiquidate = principalAmountNeeded;
}
//if liquidator reclaims the underlying asset, we make sure there is enough available collateral in the reserve
if (!_receiveAToken) {
uint256 currentAvailableCollateral = core.getReserveAvailableLiquidity(_collateral);
if (currentAvailableCollateral < maxCollateralToLiquidate) {
return (
uint256(LiquidationErrors.NOT_ENOUGH_LIQUIDITY),
"There isn't enough liquidity available to liquidate"
);
}
}
core.updateStateOnLiquidation(
_reserve,
_collateral,
_user,
vars.actualAmountToLiquidate,
maxCollateralToLiquidate,
vars.feeLiquidated,
vars.liquidatedCollateralForFee,
vars.borrowBalanceIncrease,
_receiveAToken
);
AToken collateralAtoken = AToken(core.getReserveATokenAddress(_collateral));
//if liquidator reclaims the aToken, he receives the equivalent atoken amount
if (_receiveAToken) {
collateralAtoken.transferOnLiquidation(_user, msg.sender, maxCollateralToLiquidate);
} else {
//otherwise receives the underlying asset
//burn the equivalent amount of atoken
collateralAtoken.burnOnLiquidation(_user, maxCollateralToLiquidate);
core.transferToUser(_collateral, msg.sender, maxCollateralToLiquidate);
}
//transfers the principal currency to the pool
core.transferToReserve{value: msg.value}(_reserve, msg.sender, vars.actualAmountToLiquidate);
if (vars.feeLiquidated > 0) {
//if there is enough collateral to liquidate the fee, first transfer burn an equivalent amount of
//aTokens of the user
collateralAtoken.burnOnLiquidation(_user, vars.liquidatedCollateralForFee);
//then liquidate the fee by transferring it to the fee collection address
core.liquidateFee(
_collateral,
vars.liquidatedCollateralForFee,
addressesProvider.getTokenDistributor()
);
emit OriginationFeeLiquidated(
_collateral,
_reserve,
_user,
vars.feeLiquidated,
vars.liquidatedCollateralForFee,
//solium-disable-next-line
block.timestamp
);
}
emit LiquidationCall(
_collateral,
_reserve,
_user,
vars.actualAmountToLiquidate,
maxCollateralToLiquidate,
vars.borrowBalanceIncrease,
msg.sender,
_receiveAToken,
//solium-disable-next-line
block.timestamp
);
return (uint256(LiquidationErrors.NO_ERROR), "No errors");
}
struct AvailableCollateralToLiquidateLocalVars {
uint256 userCompoundedBorrowBalance;
uint256 liquidationBonus;
uint256 collateralPrice;
uint256 principalCurrencyPrice;
uint256 maxAmountCollateralToLiquidate;
uint256 principalDecimals;
uint256 collateralDecimals;
}
/**
* @dev calculates how much of a specific collateral can be liquidated, given
* a certain amount of principal currency. This function needs to be called after
* all the checks to validate the liquidation have been performed, otherwise it might fail.
* @param _collateral the collateral to be liquidated
* @param _principal the principal currency to be liquidated
* @param _purchaseAmount the amount of principal being liquidated
* @param _userCollateralBalance the collatera balance for the specific _collateral asset of the user being liquidated
* @return collateralAmount the maximum amount that is possible to liquidated given all the liquidation constraints (user balance, close factor)
* @return principalAmountNeeded the purchase amount
**/
function calculateAvailableCollateralToLiquidate(
address _collateral,
address _principal,
uint256 _purchaseAmount,
uint256 _userCollateralBalance
) internal view returns (uint256 collateralAmount, uint256 principalAmountNeeded) {
collateralAmount = 0;
principalAmountNeeded = 0;
IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle());
// Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables
AvailableCollateralToLiquidateLocalVars memory vars;
vars.collateralPrice = oracle.getAssetPrice(_collateral);
vars.principalCurrencyPrice = oracle.getAssetPrice(_principal);
vars.liquidationBonus = core.getReserveLiquidationBonus(_collateral);
vars.principalDecimals = core.getReserveDecimals(_principal);
vars.collateralDecimals = core.getReserveDecimals(_collateral);
//this is the maximum possible amount of the selected collateral that can be liquidated, given the
//max amount of principal currency that is available for liquidation.
vars.maxAmountCollateralToLiquidate = vars
.principalCurrencyPrice
.mul(_purchaseAmount)
.mul(10 ** vars.collateralDecimals)
.div(vars.collateralPrice.mul(10 ** vars.principalDecimals))
.mul(vars.liquidationBonus)
.div(100);
if (vars.maxAmountCollateralToLiquidate > _userCollateralBalance) {
collateralAmount = _userCollateralBalance;
principalAmountNeeded = vars
.collateralPrice
.mul(collateralAmount)
.mul(10 ** vars.principalDecimals)
.div(vars.principalCurrencyPrice.mul(10 ** vars.collateralDecimals))
.mul(100)
.div(vars.liquidationBonus);
} else {
collateralAmount = vars.maxAmountCollateralToLiquidate;
principalAmountNeeded = _purchaseAmount;
}
return (collateralAmount, principalAmountNeeded);
}
}