DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: medium
Valid

Secondary short liquidation reverts due to arithmetic underflow in volatile market conditions

Summary

The ercDebtAtOraclePrice is calculated based on the cached Oracle price, which is not updated with the retrieved, potentially fresh spot price due to the 15-minute staleness limit at the beginning of the secondary liquidation call. This results in the ercDebtAtOraclePrice being greater than the short's available collateral, resulting in an underflow error when attempting to subtract the calculated ercDebtAtOraclePrice from the m.short.collateral.

Vulnerability Details

Shorts with a collateral ratio below secondaryLiquidationCR, i.e., 150% by default, can be liquidated in batches via the secondary liquidation mechanism, executed via the MarginCallSecondaryFacet.liquidateSecondary function.

All shorts within the batch are iterated, and for each short, important values are kept in memory within the MTypes.MarginCallSecondary struct, evaluated in the _setMarginCallStruct function. The collateral ratio, m.cRatio, is calculated via the LibShortRecord.getCollateralRatioSpotPrice function, based on the given oracle price.

The Oracle price is determined by the LibOracle.getSavedOrSpotOraclePrice function in line 47, which either returns the current spot price if the cached price is stale (older than 15 min) or the cached price.

153: function getSavedOrSpotOraclePrice(address asset) internal view returns (uint256) {
154: if (LibOrders.getOffsetTime() - getTime(asset) < 15 minutes) {
155: return getPrice(asset);
156: } else {
157: return getOraclePrice(asset);
158: }
159: }

Further on, the liquidation proceeds in the _secondaryLiquidationHelper function. If the short's cRatio is greater than 100% in line 166, the remaining collateral (i.e., the collateral minus the debt) is refunded. It is either refunded to the shorter if the cRatio is greater than 110% (m.minimumCR), or, otherwise, to the TAPP (address(this)).

contracts/facets/MarginCallSecondaryFacet.sol#L177

162: function _secondaryLiquidationHelper(MTypes.MarginCallSecondary memory m) private {
163: // @dev when cRatio <= 1 liquidator eats loss, so it's expected that only TAPP would call
164: m.liquidatorCollateral = m.short.collateral;
165:
166: if (m.cRatio > 1 ether) {
167: uint88 ercDebtAtOraclePrice =
168: m.short.ercDebt.mulU88(LibOracle.getPrice(m.asset)); // eth
169: m.liquidatorCollateral = ercDebtAtOraclePrice;
170:
171: // if cRatio > 110%, shorter gets remaining collateral
172: // Otherwise they take a penalty, and remaining goes to the pool
173: address remainingCollateralAddress =
174: m.cRatio > m.minimumCR ? m.shorter : address(this);
175:
176: s.vaultUser[m.vault][remainingCollateralAddress].ethEscrowed +=
177: ❌ m.short.collateral - ercDebtAtOraclePrice;
178: }
179:
180: LibShortRecord.disburseCollateral(
181: m.asset,
182: m.shorter,
183: m.short.collateral,
184: m.short.zethYieldRate,
185: m.short.updatedAt
186: );
187: LibShortRecord.deleteShortRecord(m.asset, m.shorter, m.short.id);
188: }

The value of the debt, ercDebtAtOraclePrice, is calculated based on the currently cached price, as the LibOracle.getPrice function returns the stored price.

[!NOTE]
The initially retrieved Oracle price at the beginning of the liquidation call, returned by the LibOracle.getSavedOrSpotOraclePrice function, does not store the retrieved spot price in storage if the cached price is stale.

Consequently, there are potentially two different asset prices used. The asset's spot price and the cached, stale oracle price.

Consider the case where there is a significant difference between the spot price and the cached price. This would calculate the m.cRatio based on the spot price and the ercDebtAtOraclePrice based on the cached price.

This is demonstrated in the following example:

Consider the following liquidateable short position (simplified, ignores decimal precision for this demonstration):

Collateral Debt Collateralization Ratio (based on spot price) Price ETH/USD Spot Price TOKEN/ETH Cached Price TOKEN/ETH
1 ETH 1400 TOKEN $$ 2000 0.0005 0.00075

Calculating the ercDebtAtOraclePrice with the cached oracle price 0.00075 for TOKEN/ETH, returned by the LibOracle.getPrice function, results in:

The resulting debt value, quoted in ETH, is 1.05 ETH, which is larger than the short's available collateral, m.short.collateral = 1 ETH.

This results in an arithmetic underflow error attempting to subtract the calculated ercDebtAtOraclePrice from m.short.collateral in line 177.

Specifically, this scenario occurs in the following situation:

  1. A user opens a short position with a collateral of and a debt of at TOKEN/ETH price of -> Debt in ETH: -> CR =

  2. The spot (oracle) price of TOKEN/ETH increases from to -> Debt in ETH: -> CR = (eligible for secondary liquidation - also for primary liquidation due to < 110%)

  3. New orders for the TOKEN asset are added to the order book, leading to the oracle price being updated/cached to per TOKEN

  4. ~15min after the price got updated and cached, the TOKEN/ETH spot price decreases from to . The CR improves -> CR =

  5. Secondary liquidation is attempted to liquidate the short (primary short liquidation is not possible due to the 110% CR limit)

  6. During the secondary liquidation call, m.cRatio is calculated based on the recent spot price (in step 4, due to cached price older than 15min) of -> Debt in ETH: -> CR = $ 1 / 0.7 \approx 142\%$

  7. In line 168, ercDebtAtOraclePrice is calculated based on the previously cached oracle price of ->

  8. In line 176, m.short.collateral is subtracted by ercDebtAtOraclePrice -> -> arithmetic underflow error -> reverts!

Impact

The secondary short liquidation mechanism reverts in certain market situations, forcing liquidators to wait for the CR to decrease further to be able to use the primary liquidation mechanism. This puts the overall collateral ratio and, thus the asset peg under pressure as liquidations can not be executed in a timely manner.

Tools Used

Manual Review

Recommendations

Consider also using the minimum of the m.short.collateral and ercDebtAtOraclePrice values, as similarly done in lines 204-205.

Updates

Lead Judging Commences

0xnevi Lead Judge
almost 2 years ago
0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Other
bernd Submitter
almost 2 years ago
0xnevi Lead Judge
almost 2 years ago
0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-563

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.