Incorrect calculation of burned EUROs during liquidation results in burning fewer EUROs than were minted, leading to the appearance of bad debt (and protocol loss).
The main invariant of collateralised stablecoins: Issued stable coin value <= collateral value * collateral factor
(or euro value < collateral value / collateralRate
in terms of The Standard protocol), where 0 <= collateral factor <= 1
. This invariant should be valid both for each position and for the whole protocol.
There are two cases of invariant break for a position:
collateral value * collateral factor < Issued stable coin value <= collateral value
- in such cases, the position is liquidated, collateral is sold, and its value is enough to repay (burn) all issued stablecoins.
Issued stable coin value > collateral value
- in such cases, the position is also liquidated, collateral is sold, but its value is not enough to repay (burn) all issued stablecoins:
X - issued stablecoins, Y - repaid and burnt during liquidation stablecoins where Y < X. X-Y is the protocol's bad debt. It means if all users close their positions, X-Y stablecoins are still issued. But this amount is backed by nothing.
When the protocol has bad debt, the fair price of a stablecoin = (Issued stablecoins - Bad debt) / Issued stablecoins
and it is < 1. It means that the stablecoin tends to be unpegged.
To return the peg, the protocol must buy stablecoins from the market and burn them. It means that Bad debt is the direct protocol loss.
Example of liquidation:
Preconditions:
fees = 0 to simplify the example.
Liquidation pools hold 10000 EUROs and 1 holder (to simplify the example)
_collateralRate = 1.1 (as in production).
Before liquidation:
BTC price: 10000 EUR/BTC.
User deposited 1 BTC.
User minted: 9090 EURO (maximum amount with fee = 0).
BTC price falls to: 9900 EUR/BTC -> Liquidation triggered (LiquidationPoolManager::runLiquidation()
):
1 BTC is sent to LiquidationPoolManager
(manager.liquidateVault(_tokenId);
).
1 BTC is approved to pool (ierc20.approve(pool, erc20balance);
).
distributeAssets
(LiquidationPool(pool).distributeAssets{value: ethBalance}(assets, manager.collateralRate(), manager.HUNDRED_PC());
):
portion
= 1 BTC (since only 1 holder in the pool) (uint256 _portion = asset.amount * _positionStake / stakeTotal;
).
costInEuros
= 9900 / 1.1 = 9000 (uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd) * _hundredPC / _collateralRate;
).
burnEuros
= 9000 (burnEuros += costInEuros;
).
1 BTC is sent to the pool and is available to claim by the holder (IERC20(asset.token.addr).safeTransferFrom(manager, address(this), _portion);
).
So, 9090 EUROs were minted, 9000 EUROs were burnt, still minted 9090 - 9000 = 90 EUROs. These EUROs are bad debt (backed by nothing). This bad debt will appear on any liquidations.
Each liquidation creates bad debt for the protocol.
Manual review
Liquidation contracts should take into account how many EUROs were minted by the user and burn min(minted EUROs, tokenToEuro(collateral)).
Examples:
Minted 9090 EUROs, tokenToEuro(collateral) = 9900 euros.
9090 EUROs should be burnt.
Minted 9090 EUROs, tokenToEuro(collateral) = 9000 euros.
9000 should be burnt. In this case, the protocol receives bad debt, but it was a failure of the protocol that liquidation was executed so late.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.