DittoETH

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

Minting stable assets at prices lower than actual via createLimitShort()

Summary

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.

Vulnerability Details

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.

Explaining how the createLimitShort() can be attacked by front-running and price manipulation

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.

function createLimitShort(
address asset,
uint80 price,
uint88 ercAmount,
MTypes.OrderHint[] memory orderHintArray,
uint16[] memory shortHintArray,
uint16 initialCR
) external isNotFrozen(asset) onlyValidAsset(asset) nonReentrant {
...
p.startingId = s.bids[asset][Constants.HEAD].nextId;
STypes.Order storage highestBid = s.bids[asset][p.startingId];
//@dev if match and match price is gt .5% to saved oracle in either direction, update startingShortId
if (highestBid.price >= incomingShort.price && highestBid.orderType == O.LimitBid)
{
@> LibOrders.updateOracleAndStartingShortViaThreshold( //@audit -- The createLimitShort() executes the updateOracleAndStartingShortViaThreshold(), the attacker can manipulate the incomingShort.price to be within 0.5% price deviations not to trigger the _updateOracleAndStartingShort()
@> asset, LibOracle.getPrice(asset), incomingShort, shortHintArray
@> );
}
@> p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(asset); //@audit -- 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
//@dev reading spot oracle price
@> if (incomingShort.price < p.oraclePrice) { //@audit -- Since the p.oraclePrice is outdated (lower than the actual), the sellMatchAlgo() will be executed instead of the addShort()
LibOrders.addShort(asset, incomingShort, orderHintArray);
} else {
@> LibOrders.sellMatchAlgo(asset, incomingShort, orderHintArray, p.minAskEth); //@audit -- The createLimitShort() executes the sellMatchAlgo() because of the outdated p.oraclePrice
}
emit Events.CreateShort(
asset, msg.sender, incomingShort.id, incomingShort.creationTime
);
}
  • 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

Impact

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.

Tools Used

Manual Review

Recommendations

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.

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.