Issue M-1: Liquidators can routinely leave deficit not formed setting gas cost to cover normal liquidations only
Source: #197
hyh
Liquidators can use tx gas limit to accept a loss instead of performing bad debt cleanup. When the latter is not profitable all the liquidators will avoid the last, MIN_LEFTOVER_BASE
sized, part of the collateral in their calls and if one's call is about to be executed with higher gas usage this essentially means that a liquidator is not first and will obtain only MIN_LEFTOVER_BASE
liquidation bonus (LB), paying the full deficit formation cost. If that bears a loss higher than a loss of just paying the non-deficit liquidation gas price the liquidators will routinely set the gas limit to cover it only, removing the possibility of bad debt clearing.
Bad debt cleanup liquidations and normal liquidations have substantially different gas costs, while former is far less profitable for a liquidator (it's lower collateral left at that point). So in a competing environment where it's profitable to leave some collateral intact, the first liquidator get the main funds, while the late transactions will be executed at a loss, not being able to stop the execution otherwise, they can set gas costs to cover normal liquidations only whenever this loss be still more profitable than receiving a liquidation bonus off the small collateral leftover, while performing full bad debt clearing.
These situations will essentially mean that borrower has paid full extra cost in the form of the nearly full position LB, while bad debt wasn't cleared and might not be cleared at all in the future, depending of the market state. I.e. liquidators can get the extra LB, creating more bad debt comparing to 3.2 and not clearing it.
The creating more bad debt part comes from the fact that incentive to leave the smallest possible collateral leftover comes from the existence of bad debt cleanup logic, not present before, i.e. since 3.3 for one-collateral-many-debts borrowers if a liquidator don't leave something, they have to pay for _burnBadDebt()
:
LiquidationLogic.sol#L408-L410
if (hasNoCollateralLeft && userConfig.isBorrowingAny()) {
> _burnBadDebt(reservesData, reservesList, userConfig, params.reservesCount, params.user);
}
Which can be very costly (the 492078 - 392439
gas cost difference is multiplied by the number of additional active bad debt reserves):
> "liquidationCall: deficit on liquidated asset": "392439",
> "liquidationCall: deficit on liquidated asset + other asset": "492078",
"liquidationCall: full liquidation": "392439",
"liquidationCall: full liquidation and receive ATokens": "368797",
"liquidationCall: partial liquidation": "383954",
"liquidationCall: partial liquidation and receive ATokens": "360308",
Borrower being liquidated has one collateral, with other supplies being not used as collaterals, and a number of debts. This can be quite common since 3.3 spreading over many debt reserves comes as a natural remedy from 50%
rule change from being debt reserve wise to be the whole position wise. Liquidating one debt will bring HF up and can block the liquidations of all the other debts, while having one debt reserve a borrower can be liquidated fully at once.
Gas price is high on liquidations and it's calm conditions level remains high enough so LB * MIN_LEFTOVER_BASE
isn't enough to cover for gas costs of the deficit formations of all debt reserves of the borrower (this is done all in one atomically).
Liquidators restrict gas spend of the transactions, so if they are winning the first place in a block it will be executed, but if they are not it will be reverted bearing the fixed loss of non-deficit liquidation gas budget. Whichever it will be depends solely on the state of the pool, tx parameters are the same.
This allows liquidators to receive nearly full position LB, but not paying for deficit formation, essentially stealing from the borrowers 3.3 introduced extra LB.
Since allowing the 50%
of all the total debt to be liquidated at once is a extra payoff for liquidators at the expense of the borrowers in order to provide bad debt clearing and deficit formation, the failure to do so is a direct loss for the borrowers from the 3.3 release. I.e. in the described circumstances nothing changes bad debt vice, it's still unrealized and require manual DAO intervention (repaying on behalf), but borrowers now lose more LB to liquidators.
-
L1 borrower Bob have only one collateral,
totalAmount
of it, and have a number of different debt reserves, say7
. A market wide down move just happened resulting with increased gas price and his collateral losing value sharply so it will no longer cover his biggest debt reserve's position after an addition of the liquidator incentive, i.e. his cumulative debt position becomes liquidable with bad debt coming from all the reserves. -
Liquidators Alice and Max both calculated the expected profit of two variants: liquidating the whole collateral and leaving
MIN_LEFTOVER_BASE
of it. Since the gas costs are elevated the total cost of bad debt cleanup for Bob's position is greater than liquidator incentive coming fromMIN_LEFTOVER_BASE
part of the collateral, so both Alice and Max simultaneously run liquidations calls withdebtToCover = totalAmount - MIN_LEFTOVER_BASE
, i.e. both want to leave theMIN_LEFTOVER_BASE
part of Bob's collateral intact. -
Alice outbid Max for the block placement and run the liquidation, leaving Max tx to deal with only
MIN_LEFTOVER_BASE
of collateral, while it also havingdebtToCover = totalAmount - MIN_LEFTOVER_BASE
. It will be reduced toMIN_LEFTOVER_BASE
and Max's tx will be executed at loss. However, the loss comes from the extra gas costs of bad debt cleanup. -
Knowing that, both Alice and Max will set gas limit for their transactions to cover non-bad-debt case only. Say, in line with the latest snapshot if all the no-bad-debt liquidations cost around
400k
and each bad debt clean-up cost around100k
per asset, both Alice and Max can set the limit at410k
if paying that much without receiving liquidation bonus if less expensive than clearing all the bad debt assets with the full cost, while receiving theMIN_LEFTOVER_BASE * LB
payoff (let's omit protocol part for simplicity). What is more profitable between these two depends on the exact gas cost, price per action in the release version, native token price, collateral LB and the number of debt reserves Bob have. -
Depending on these variables bad debt might be profitable or not to be cleared even when gas price abates to its current norm.
Consider evaluating bad debt clearing scenarios for the various min gas prices, collateral LBs and big enough number of debt assets to be cleared. That is, LB coming from the one size fits all MIN_LEFTOVER_BASE
has to cover not just a normal liquidations, but a cleanup of may, e.g. 7-10
debt assets, with high enough low activity gas price, e.g. 15 Gwei
, and a low enough stable collateral asset LB, e.g. 4.5%
, in an environment of elevated ETH prices, e.g. ETH at 5k
, 10k
. Now it looks to be somewhat lower than needed, e.g. liquidating 7
assets at 15 Gwei
at the ETH price as of time of this report costs nearly twice more than 4.5% * 1000 = 45 USD
LB.
Bearing in mind the outlined necessity to cover for these inherently costly scenarios consider rising MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD
.
There are some additional considerations:
-
MIN_LEFTOVER_BASE
being higher than current minimal profitably liquidable debt only forces liquidation to be less fragmented, as low value positions can still be liquidated in full onceHF <= CLOSE_FACTOR_HF_THRESHOLD
: it forbids the case whenHF > CLOSE_FACTOR_HF_THRESHOLD
and0.5 * totalDebtInBaseCurrency < MIN_LEFTOVER_BASE
, and it's profitable to liquidate0.5 * totalDebtInBaseCurrency
, but not profitable to liquidatetotalDebtInBaseCurrency - MIN_LEFTOVER_BASE
, i.e. minMIN_LEFTOVER_BASE
leftover requirement pushes the liquidation amount lower than50%
of the whole portfolio and out of the profitable range so liquidation won't happen until HF deteriorates further, which puts some pressure on the overall health of the protocol -
MIN_LEFTOVER_BASE
being lower than current minimal profitably liquidable debt directly allows the creation of many leftover positions that won't be liquidated and also allows for more cases of leaving some collateral behind just to avoid bad debt cleanup. WhenMIN_LEFTOVER_BASE
is big it forbids some material share of such cases (a position will generate bad debt if collateral be zeroed, so the liquidator would like to leave some, but can't as it violate theMIN_LEFTOVER_BASE
leftover: this is as frequent asMIN_LEFTOVER_BASE
is big, say zero requirement won't catch any such cases) and liquidators either game the system as described or pay for bad debt cleanup as designed
That is, from overall design perspective it's a tradeoff, but bigger MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD
and MIN_LEFTOVER_BASE = MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD / 2
look more plausible.
Issue M-2: Liquidator can avoid resolving bad debt with dust supply/transfer while seizing all the borrower's collateral
Source: #203
bughuntoor, hyh, pkqs90
Whenever a position with one collateral and many debt reserves is up for liquidation with claiming all the collateral, its debt reserves will be deemed bad debt and cleaned up during executeLiquidationCall()
, which will bear a significant cost for a liquidator.
To avoid that the liquidator can create an additional collateral for the borrower. For example, can send a dust amount of non-isolated mode aToken not used by them yet, enabling it as a collateral via executeFinalizeTransfer()
. This can be used by liquidators routinely as profit enhancement and leaves all the bad debt intact with deficit not formed.
When vars.totalCollateralInBaseCurrency > vars.collateralToLiquidateInBaseCurrency
in executeLiquidationCall()
it is hasNoCollateralLeft == false
and bad debt clean-up is avoided. For a bad debt bearing position with many debt reserves, liquidators will do that as long as this is profitable, which can frequently be the case on L1.
Borrower being liquidated has one collateral, with other supplies being not used as collaterals, and a number of debts. This can be quite common since 3.3 spreading over many debt reserves comes as a natural remedy from 50% rule change from being debt reserve wise to be the whole position wise.
Gas price is high on liquidations so paying for dust aToken transfer or supply on behalf is cheaper than covering gas costs of the deficit formation for all debt reserves of the borrower.
The goal is to trigger setUsingAsCollateral(id, true)
with some additional action that doesn't require borrower participation. This can be supply with onBehalfOf = borrower
on straightforward aToken transfer, which is cheaper:
executeFinalizeTransfer()
, SupplyLogic.sol#L216-L230:
if (params.balanceToBefore == 0) {
DataTypes.UserConfigurationMap storage toConfig = usersConfig[params.to];
if (
ValidationLogic.validateAutomaticUseAsCollateral(
reservesData,
reservesList,
toConfig,
reserve.configuration,
reserve.aTokenAddress
)
) {
>> toConfig.setUsingAsCollateral(reserveId, true);
emit ReserveUsedAsCollateralEnabled(params.asset, params.to);
}
}
Since allowing the 50% of all the total debt to be liquidated at once is a extra payoff for liquidators at the expense of the borrowers in order to provide bad debt clearing and deficit formation, the failure to do so is a direct loss for the borrowers from the 3.3 release. I.e. in the described circumstances nothing changes bad debt vice, it's still unrealized and require manual DAO intervention (repaying on behalf), but borrowers now lose more LB to liquidators.
According to the contest snapshot it's about 145k
for aToken transfer with enabling the collateral. Supply looks to be more expensive, 176k
.
"full amount; receiver: ->enableCollateral": "144881",
It's about 100k per an additional asset bad debt clean-up. Whenever a bad debt bearing borrower have more than 2 debt reserves with one of them capable to take all the collateral it's profitable to transfer aToken and avoid the cleanup (100*k > 145 if k > 1
, where k
is number of additional debt reserves).
Since user can always enable the collateral manually one way to control for the issue is to require that automatic use happens on non-dust amount addition only, e.g.:
function validateAutomaticUseAsCollateral(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ReserveConfigurationMap memory reserveConfig,
+ uint256 amountInBaseCurrency,
address aTokenAddress
) internal view returns (bool) {
if (reserveConfig.getDebtCeiling() != 0) {
// ensures only the ISOLATED_COLLATERAL_SUPPLIER_ROLE can enable collateral as side-effect of an action
IPoolAddressesProvider addressesProvider = IncentivizedERC20(aTokenAddress)
.POOL()
.ADDRESSES_PROVIDER();
if (
!IAccessControl(addressesProvider.getACLManager()).hasRole(
ISOLATED_COLLATERAL_SUPPLIER_ROLE,
msg.sender
)
) return false;
- }
+ } else {
+ // ensures that amount that triggered the action is not below minimum
+ if (amountInBaseCurrency < LiquidationLogic.MIN_LEFTOVER_BASE) return false;
+ }
return validateUseAsCollateral(reservesData, reservesList, userConfig, reserveConfig);
}
All the uses of validateAutomaticUseAsCollateral
will need to supply the base currency equivalent amount for the action, e.g.:
if (params.balanceToBefore == 0) {
DataTypes.UserConfigurationMap storage toConfig = usersConfig[params.to];
+ uint256 assetPrice = IPriceOracleGetter(params.oracle).getAssetPrice(params.asset);
+ uint256 assetUnit = 10 ** reserve.configuration.getDecimals();
+ uint256 amountInBaseCurrency = (params.amount * assetPrice) / assetUnit;
if (
ValidationLogic.validateAutomaticUseAsCollateral(
reservesData,
reservesList,
toConfig,
reserve.configuration,
+ amountInBaseCurrency,
reserve.aTokenAddress
)
) {
toConfig.setUsingAsCollateral(reserveId, true);
emit ReserveUsedAsCollateralEnabled(params.asset, params.to);
}
}