DittoETH

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

Front-running attacks on liquidateSecondary() to receive collateral more than expected

Summary

The MarginCallSecondaryFacet::liquidateSecondary() is vulnerable to front-running attacks, as the function calculates the buy-back's collateral amount using a cached price, which can be front-run by attackers.

As a result, an attacker can execute the liquidateSecondary() to liquidate Short positions and receive the buy-back's collateral amount more than they should.

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.

The function will call the _secondaryLiquidationHelper() to perform the full liquidation on target Short positions and call the _secondaryLiquidationHelperPartialTapp() to perform the partial liquidation on the TAPP's Short positions.

After liquidating each Short position, the buy-back's collateral amount that the liquidator will receive will be accumulated by the variable liquidatorCollateral. The liquidator will receive the total accumulated collateral after the loop of liquidating Short positions finishes.

However, the liquidateSecondary() is vulnerable to front-running attacks. Both the _secondaryLiquidationHelper() and _secondaryLiquidationHelperPartialTapp() calculate the buy-back's collateral amount using a cached price retrieved from the LibOracle::getPrice() in L167 - L168 and L204 - L205, respectively.

Let's say Chainlink has updated the price to be lower than the protocol's oracle price (cached). An attacker can front-run the protocol's oracle price update and execute the liquidateSecondary() to liquidate Short positions. Since the protocol's oracle still retains the higher price (higher debt than the actual), the attacker will receive the buy-back's collateral amount more than expected.

function liquidateSecondary(
address asset,
MTypes.BatchMC[] memory batches,
uint88 liquidateAmount,
bool isWallet
) external onlyValidAsset(asset) isNotFrozen(asset) nonReentrant {
...
uint88 liquidatorCollateral;
uint88 liquidateAmountLeft = liquidateAmount;
for (uint256 i; i < batches.length;) {
...
if (partialTappLiquidation) {
// Partial liquidation of TAPP short
@> _secondaryLiquidationHelperPartialTapp(m); //@audit -- The liquidateSecondary() executes the _secondaryLiquidationHelperPartialTapp()
} else {
// Full liquidation
@> _secondaryLiquidationHelper(m); //@audit -- The liquidateSecondary() executes the _secondaryLiquidationHelper()
}
// Update in memory for final state change after loops
@> liquidatorCollateral += m.liquidatorCollateral; //@audit -- The liquidateSecondary() updates the accumulated liquidatorCollateral
liquidateAmountLeft -= m.short.ercDebt;
if (liquidateAmountLeft == 0) break;
}
...
// Update finalized state changes
s.asset[asset].ercDebt -= liquidateAmount - liquidateAmountLeft;
@> s.vaultUser[m.vault][msg.sender].ethEscrowed += liquidatorCollateral; //@audit -- The liquidator receives the total collateral (zETH) according to the accumulated liquidatorCollateral
emit Events.LiquidateSecondary(asset, batches, msg.sender, isWallet);
}
...
function _secondaryLiquidationHelper(MTypes.MarginCallSecondary memory m) private {
// @dev when cRatio <= 1 liquidator eats loss, so it's expected that only TAPP would call
m.liquidatorCollateral = m.short.collateral;
if (m.cRatio > 1 ether) {
@> uint88 ercDebtAtOraclePrice =
@> m.short.ercDebt.mulU88(LibOracle.getPrice(m.asset)); // eth //@audit -- The _secondaryLiquidationHelper() calculates the ercDebtAtOraclePrice using the cached price (which the attacker can front-run its update)
@> m.liquidatorCollateral = ercDebtAtOraclePrice; //@audit -- The calculated ercDebtAtOraclePrice is assigned to the m.liquidatorCollateral
// if cRatio > 110%, shorter gets remaining collateral
// Otherwise they take a penalty, and remaining goes to the pool
address remainingCollateralAddress =
m.cRatio > m.minimumCR ? m.shorter : address(this);
@> s.vaultUser[m.vault][remainingCollateralAddress].ethEscrowed +=
@> m.short.collateral - ercDebtAtOraclePrice; //@audit -- The more ercDebtAtOraclePrice, the less collateral the remainingCollateralCollector will receive
}
...
}
...
function _secondaryLiquidationHelperPartialTapp(MTypes.MarginCallSecondary memory m)
private
{
STypes.ShortRecord storage short =
s.shortRecords[m.asset][address(this)][m.short.id];
// Update erc balance
short.ercDebt -= m.short.ercDebt; // @dev m.short.ercDebt was updated earlier to equal erc filled
// Update eth balance
// If c-ratio < 1 then it's possible to lose eth owed over short collateral
@> m.liquidatorCollateral =
@> min88(m.short.ercDebt.mul(LibOracle.getPrice(m.asset)), m.short.collateral); //@audit -- The _secondaryLiquidationHelperPartialTapp() calculates the m.liquidatorCollateral using the cached price (which the attacker can front-run its update)
@> short.collateral -= m.liquidatorCollateral; //@audit -- The TAPP's collateral will be reduced by the calculated m.liquidatorCollateral
LibShortRecord.disburseCollateral(
m.asset,
m.shorter,
m.liquidatorCollateral,
m.short.zethYieldRate,
m.short.updatedAt
);
}
  • The liquidateSecondary() executes the _secondaryLiquidationHelperPartialTapp(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L97

  • The liquidateSecondary() executes the _secondaryLiquidationHelper(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L100

  • The liquidateSecondary() updates the accumulated liquidatorCollateral: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L104

  • The liquidator receives the total collateral (zETH) according to the accumulated liquidatorCollateral: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L115

  • The _secondaryLiquidationHelper() calculates the ercDebtAtOraclePrice using the cached price (which the attacker can front-run its update): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L167-L168

  • The calculated ercDebtAtOraclePrice is assigned to the m.liquidatorCollateral: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L169

  • The more ercDebtAtOraclePrice, the less collateral the remainingCollateralCollector will receive: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L176-L177

  • The _secondaryLiquidationHelperPartialTapp() calculates the m.liquidatorCollateral using the cached price (which the attacker can front-run its update): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L204-L205

  • The TAPP's collateral will be reduced by the calculated m.liquidatorCollateral: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallSecondaryFacet.sol#L206

Impact

The attack from this vulnerability can increase more bad debt to both the liquidated shorters and the Ditto protocol.

Specifically, the liquidated shorters and the protocol's TAPP Short positions will directly lose their collateral (zETH) to the attacker. Further, the protocol can become insolvent, and the protocol's minted stable assets (e.g., cUSD) can eventually become de-pegged.

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
Validated
Assigned finding tags:

finding-563

Support

FAQs

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