The ExitShortFacet::exitShort()
is vulnerable to front-running and price manipulation attacks, leading to the minting of stable assets (e.g., cUSD) at prices lower than the actual price of the targeted asset (fed by Chainlink).
The root cause of this vulnerability is that an attacker can place a forced Bid order to match Short orders that have prices below the actual price. As a result, the attack can eventually result in the de-pegging of the minted assets.
A shorter can exit their Short position by executing the exitShort()
. Under the hood, the target Short's collateral (zETH) will be used to buy back the position's ercDebt
(e.g., cUSD) using the protocol's order book (by creating a "forced Bid" on the target Short position). The remaining collateral after exiting the Short will be returned to the shorter.
In order to extract the maximal value, an attacker (i.e., shorter) must expect to buy back their ercDebt
by spending the least amount of collateral.
In case the forced Bid order matches Ask orders, the current oracle price will not be taken into account. Conversely, if the forced Bid order matches Short orders, those matched Short orders must have prices at or above the oracle price. This constraint maintains the minting of stable assets to the actual price of the targeted asset (e.g., cUSD is pegged with USD) unless the minted assets could eventually be de-pegged.
However, the exitShort()
is vulnerable to front-running and price manipulation attacks, leading to the minting of stable assets at prices lower than the actual price of the targeted asset.
To elaborate on the vulnerability, please consider the following section.
Even if the exitShort()
will execute the LibOrders::updateOracleAndStartingShortViaTimeBidOnly()
that will update the protocol's oracle price (cache) and start the process of finding the new startingShortId
if the last updated timestamp is more than or equal to 15 minutes (i.e., oracleFrequency == OF.FifteenMinutes
).
The updateOracleAndStartingShortViaTimeBidOnly()
cannot guarantee that suddenly after the price fed by Chainlink has been updated, the protocol's cached price will be immediately updated (because of the condition: timeDiff >= 15 minutes
).
Let's say the price updated by Chainlink is higher than the protocol's cached price. The attacker can front-run the protocol's oracle price update and execute the exitShort()
to place the forced Bid order with price == the protocol's cached price (stale price). Because of the condition: timeDiff >= 15 minutes
, the updateOracleAndStartingShortViaTimeBidOnly()
will not trigger the _updateOracleAndStartingShort()
. Therefore, the protocol's cached price will not be updated.
After that, the exitShort()
will invoke the BidOrdersFacet::createForcedBid()
.
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ExitShortFacet.sol#L159-L161
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ExitShortFacet.sol#L210-L212
The BidOrdersFacet::createForcedBid()
will execute the _createBid()
to create a "forced Bid" on the target Short position.
Once the _createBid()
loads the oracle price (b.oraclePrice
) from the cached storage, the stale price will be consumed.
Even though the _createBid()
will execute the LibOrders::updateOracleAndStartingShortViaThreshold()
to make sure that if the price difference between the forced Bid order's and the protocol's cached oracle is greater than 0.5% deviation , the _updateOracleAndStartingShort()
will be triggered to update the protocol's oracle price and start the process of finding the new startingShortId
.
Since the attacker places the forced Bid order with price == the protocol's cached price, the execution of the _updateOracleAndStartingShort()
will be bypassed.
Eventually, the _createBid()
will execute the bidMatchAlgo()
to match the attacker's forced Bid order with Sell orders (Ask or Short orders) whose prices are less than the actual price (Chainlink). As described earlier, matching Ask orders whose prices are below the actual price is the correct implementation.
However, the bidMatchAlgo()
also matches the forced Bid order with Short orders whose prices are below the actual price (fed by Chainlink) since the protocol's cached price is outdated. Subsequently, the protocol will mint the stable asset at the price below the actual price. In other words, the minting mechanism will eventually catalyze the minted asset to depeg.
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L92
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L129
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L144-L146
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L148
After the target Short position's ercDebt
is entirely bought back, the remaining collateral (zETH) will be returned to the attacker. The attack can make the attacker extract the maximal value because their forced Bid order will match all lowest Ask and Short orders whose prices are less than the actual price.
The exitShort()
is vulnerable to front-running and price manipulation attacks, leading to the minting of stable assets (e.g., cUSD) at prices lower than the actual price of the targeted asset (fed by Chainlink).
The attacker (i.e., shorter) will receive the maximal collateral (zETH) back.
However, the impact on the Ditto
protocol is enormous since it can finally result in the de-pegging of the minted assets.
Manual Review
Since the cached oracle price is prone to front-running and price manipulation 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.