The exitShortWallet()
, exitShortErcEscrowed()
, and exitShort()
of the ExitShortFacet
contract are vulnerable to front-running attacks, as the functions calculate the Short's collateral ratio using a cached price, which can be front-run by attackers.
As a result, an attacker can partially exit their flagged Short position by spending less minted asset token (cUSD) than the actual (to buy back the Short's debt) to reset the liquidation flag from their Short position.
Note: the vulnerability affects the
exitShortWallet()
,exitShortErcEscrowed()
, andexitShort()
of theExitShortFacet
contract, but only theexitShortWallet()
will be explained in this report, as the explanation for the rest functions is the same.
A shorter can execute the exitShortWallet()
to partially or fully exit their Short position. To exit a Short, a shorter must give the function some minted asset token (e.g., cUSD) to buy back the Short's debt (ercDebt
). If the partial exit is executed on the Short, the exitShortWallet()
will invoke the LibShortRecord::maybeResetFlag()
.
The maybeResetFlag()
will calculate the Short's collateral ratio (cRatio
) by calling the LibShortRecord::getCollateralRatio()
. If the resulting cRatio
is above the primaryLiquidationCR
threshold, the function will invoke the LibShortRecord::resetFlag()
to reset the liquidation flag of the Short position.
However, the exitShortWallet()
is vulnerable to front-running attacks since the getCollateralRatio()
will calculate the cRatio
using a cached price retrieved from the LibOracle::getPrice()
.
Let's say Chainlink has updated the price to be higher than the protocol's oracle price (cached). An attacker (i.e., shorter) can front-run the protocol's oracle price update and execute the exitShortWallet()
to partially exit their flagged Short. Since the protocol's oracle still retains the lower price (lower debt than the actual), the attacker can spend less minted asset token (cUSD) than the actual (to buy back the Short's debt) to reset the liquidation flag from their Short position, deferring their Short position from being liquidated by the MarginCallPrimaryFacet::liquidate()
(Primary liquidation method).
Note that to liquidate the attacker's Short again, a flagger must flag that Short again. It will take at least 10 hours (the default of the firstLiquidationTime
parameter) for the flagged Short to be liquidatable.
Consequently, this vulnerability can indirectly increase bad debt to the Ditto
protocol. The protocol can become insolvent, and the protocol's minted stable assets (e.g., cUSD) can eventually be de-pegged.
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ExitShortFacet.sol#L75
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/libraries/LibShortRecord.sol#L416
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/libraries/LibShortRecord.sol#L419
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/libraries/LibShortRecord.sol#L27
Note: the vulnerability affects the
exitShortWallet()
,exitShortErcEscrowed()
, andexitShort()
of theExitShortFacet
contract, but only theexitShortWallet()
will be explained in this report, as the explanation for the rest functions is the same.
This vulnerability enables an attacker (i.e., shorter) to partially exit their flagged Short position by spending less minted asset token (cUSD) than the actual (to buy back the Short's debt) to reset the liquidation flag from their Short position, deferring their Short position from being liquidated (Primary liquidation method).
Note that to liquidate the attacker's Short again, a flagger must flag that Short again. It will take at least 10 hours (the default of the firstLiquidationTime
parameter) for the flagged Short to be liquidatable.
Consequently, this vulnerability can indirectly increase bad debt to the Ditto
protocol. The protocol can become insolvent, and the protocol's minted stable assets (e.g., cUSD) can eventually be de-pegged.
Manual Review
Since the cached oracle price is prone to front-running attacks, always execute the LibOracle::getOraclePrice()
to get the accurate price from Chainlink.
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.