High-volatility ticks can cause bank run due to negative liquidations
Description
The liquidation mechanism in Market.sol calculates the maintenance (minimum collateral) and liquidation fee for a given position as follows:
function liquidationFee(
Position memory self,
OracleVersion memory latestVersion,
RiskParameter memory riskParameter
) internal pure returns (UFixed6) {
return maintenance(self, latestVersion, riskParameter)
.mul(riskParameter.liquidationFee)
.min(riskParameter.maxLiquidationFee)
.max(riskParameter.minLiquidationFee);
}
function maintenance(
Position memory self,
OracleVersion memory latestVersion,
RiskParameter memory riskParameter
) internal pure returns (UFixed6) {
if (magnitude(self).isZero()) return UFixed6Lib.ZERO;
return magnitude(self)
.mul(latestVersion.price.abs())
.mul(riskParameter.maintenance)
.max(riskParameter.minMaintenance);
}
Since the liquidation fee is not constrained to be less than the collateral, a high-volatility tick can cause the liquidation fee to exceed the deposited collateral. When this happens, the liquidation itself will cause the position to end with negative collateral. So, if a user opens a position with collateral very close to maintenance, the position can then be self-liquidated for more than the deposited collateral following a volatile tick.
Impact
We created a proof of concept (POC) for this bug (section ). In this POC, we demonstrate a scenario where the first depositor can self-liquidate the position for more than their deposit, effectively stealing other users' funds and making the market insolvent.
An excerpt of the POC output is shown below:
User deposits collateral
Deposited collateral: 1000000000
Volatile click changes price to 1.5
Position liquidated
collateral after liquidation: -1001000000
token earned by liquidator: 1001000000
attack successful
It is to be noted that although an organic bank run scenario is possible, it does require a fairly volatile tick from the oracle under appropriate tuning parameters.
For example, for a power two oracle with riskParameter.liquidationFee = 0.5
, we would need a 48% price change between two subsequent oracle ticks. With riskParameter.liquidationFee = 0.7
, the required volatility is 18%. These values, while feasible, are still rare in practice.
There are two other possible exploitation scenarios.
It may be used as a backdoor by a malicious oracle operator to drain the market relying on it.
It may lead to a malicious user trying to intentionally exploit this as an infinite money glitch by opening a number of positions and self-liquidating them. However, such a user would need to anticipate an incoming volatile tick.
Recommendations
A permanent fix would require liquidations to be capped at the total deposited assets of a user. However, the current Perennial design does not track the total deposit for an account, so implementing that would require a considerable amount of rewrites. For now, this possibility should be minimized via appropriate parameter tuning on a per-market level.