DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: high
Invalid

Liquidating short positions with the collateral ratio above the threshold

Summary

The MarginCallSecondaryFacet::liquidateSecondary() is vulnerable to front-running attacks, allowing attackers to liquidate some Short positions with the collateral ratio above the secondaryLiquidationCR threshold.

Vulnerability Details

The liquidateSecondary() liquidates the Short positions (batches) using the liquidator's ERC (cUSD) to buy back the liquidated Shorts' debt. The liquidator will receive the collateral (zETH) for exchange. This function can be invoked by anyone without the need to flag any Short positions.

The liquidateSecondary() executes LibOracle::getSavedOrSpotOraclePrice() to query for the latest price from Chainlink if the last updated timestamp is more than or equal to 15 minutes. Otherwise, the function will return the cached oracle price. With the 15-minute update window, an attacker has room to front-run the protocol's oracle price update. In other words, soon after Chainlink has updated its price, the getSavedOrSpotOraclePrice() cannot guarantee that it will immediately fetch the latest price from Chainlink.

Let's say Chainlink has updated the price to be lower than the protocol's oracle price (cached). The attacker can front-run the protocol's oracle price update and execute the liquidateSecondary() to liquidate the targeted Short positions.

This way, the stale cached price (oraclePrice) will be passed to the _setMarginCallStruct(). The _setMarginCallStruct() will use the oraclePrice to calculate the target Short's collateral ratio (m.cRatio). Since the oraclePrice contains a higher price (stale) than Chainlink, the resulting calculated m.cRatio will be lower than the actual.

When the liquidateSecondary() verifies the liquidation eligibility of the target Short position, that Short position could be considered as liquidatable (even if that Short position might have the cRatio above the secondaryLiquidationCR threshold in reality).

As a result, the attacker can liquidate some Short positions with the cRatio above the secondaryLiquidationCR threshold.

function liquidateSecondary(
address asset,
MTypes.BatchMC[] memory batches,
uint88 liquidateAmount,
bool isWallet
) external onlyValidAsset(asset) isNotFrozen(asset) nonReentrant {
STypes.AssetUser storage AssetUser = s.assetUser[asset][msg.sender];
MTypes.MarginCallSecondary memory m;
uint256 minimumCR = LibAsset.minimumCR(asset);
@> uint256 oraclePrice = LibOracle.getSavedOrSpotOraclePrice(asset);
uint256 secondaryLiquidationCR = LibAsset.secondaryLiquidationCR(asset);
uint88 liquidatorCollateral;
uint88 liquidateAmountLeft = liquidateAmount;
for (uint256 i; i < batches.length;) {
@> m = _setMarginCallStruct(
@> asset, batches[i].shorter, batches[i].shortId, minimumCR, oraclePrice
@> );
unchecked {
++i;
}
// If ineligible, skip to the next shortrecord instead of reverting
if (
@> m.shorter == msg.sender || m.cRatio > secondaryLiquidationCR
|| m.short.status == SR.Cancelled
|| m.short.id >= s.assetUser[asset][m.shorter].shortRecordId
|| m.short.id < Constants.SHORT_STARTING_ID
|| (m.shorter != address(this) && liquidateAmountLeft < m.short.ercDebt)
) {
continue;
}
... //@audit -- liquidate Short position (m.short.id)
}
...
}
...
function _setMarginCallStruct(
address asset,
address shorter,
uint8 id,
uint256 minimumCR,
uint256 oraclePrice
) private returns (MTypes.MarginCallSecondary memory) {
LibShortRecord.updateErcDebt(asset, shorter, id);
MTypes.MarginCallSecondary memory m;
m.asset = asset;
m.short = s.shortRecords[asset][shorter][id];
m.vault = s.asset[asset].vault;
m.shorter = shorter;
m.minimumCR = minimumCR;
@> m.cRatio = m.short.getCollateralRatioSpotPrice(oraclePrice);
return m;
}
  • https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L47

  • https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L53-L55

  • https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L63

  • https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L144

Impact

The attacker can liquidate some Short positions with the cRatio above the secondaryLiquidationCR threshold.

This vulnerability will directly affect the liquidated shorters (i.e., victims) as they will lose a lot of collateral from the liquidation (note that their positions should not get liquidated yet in reality).

Tools Used

Manual Review

Recommendations

Since the cached oracle price is prone to front-running attacks, always execute the LibOracle::getOraclePrice() to get the accurate price from Chainlink.

Updates

Lead Judging Commences

0xnevi Lead Judge
almost 2 years ago
0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Other
serialcoder Submitter
almost 2 years ago
0xnevi Lead Judge
almost 2 years ago
0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

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