The ShortOrdersFacet::createLimitShort()
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 Short order with a price below the actual price to match Bid orders. As a result, the attack can eventually result in the de-pegging of the minted assets.
In order to incentivize shorters, only matched shorters will be eligible for yield. Thus, shorters must do their best for their Short orders to be matched. Unlike Ask orders which can match Bid orders regardless of the current oracle price. Conversely, in the case of Short orders, the 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 createLimitShort()
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. The attack can eventually result in the de-pegging of the minted assets.
To elaborate on the vulnerability, please consider the following section.
Even though the createLimitShort()
will execute the LibOrders::updateOracleAndStartingShortViaThreshold()
to make sure that if the price difference between the Short 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
.
Assuming that Chainlink has updated its reported price to be higher than the protocol's cached price. An attacker can front-run the protocol's oracle price update and execute the createLimitShort()
to place a Short order with price == the protocol's cached price (stale price). Since the Short order has its price == the protocol's cached price, the execution of the _updateOracleAndStartingShort()
will be bypassed. Therefore, the protocol's cached price will not be updated.
Later, the createLimitShort()
will execute 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, the 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.
Once the createLimitShort()
processes on the if statement in L83, the function will execute the LibOrders::sellMatchAlgo()
to match the attacker's Short order instead of executing the LibOrders::addShort()
to just add the Short order on the market. This is because the protocol's oracle price (p.oraclePrice
) is outdated(p.oraclePrice
< Chainlink's price).
Eventually, the sellMatchAlgo()
will be executed to match the attacker's Short order with Bid orders, even if the Short order will have a price less than the actual price (Chainlink).
As described earlier, matching Short orders whose prices are below the actual price (fed by Chainlink) will render the protocol to mint the stable asset at a price below the actual price. In other words, the minting mechanism will eventually catalyze the minted asset to depeg.
The createLimitShort() executes the updateOracleAndStartingShortViaThreshold(), the attacker can manipulate the incomingShort.price to be within 0.5% price deviations not to trigger the _updateOracleAndStartingShort()
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ShortOrdersFacet.sol#L76-L78
The getSavedOrSpotOraclePrice() will fetch the latest price from Chainlink with oracleFrequency == 15 minutes. Otherwise, the cached price will be returned instead. With this constraint, the attacker can front-run the protocol from getting the updated price
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ShortOrdersFacet.sol#L81
Since the p.oraclePrice is outdated (lower than the actual), the sellMatchAlgo() will be executed instead of the addShort()
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ShortOrdersFacet.sol#L83
The createLimitShort() executes the sellMatchAlgo() because of the outdated p.oraclePrice
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ShortOrdersFacet.sol#L86
The createLimitShort()
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 get a yield from their matched Short orders. But, the impact 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.