DittoETH

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

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

Summary

The BidOrdersFacet::createBid() 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 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.

Vulnerability Details

In order to extract the maximal value, a bidder must expect to buy the stable asset (e.g., cUSD) by spending the least amount of zETH. In case a Bid order matches Ask orders, the current oracle price will not be taken into account. Conversely, if a 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 createBid() 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 createBid() can be attacked by front-running and price manipulation

Even if the createBid() 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 1 hour (i.e., oracleFrequency == OF.OneHour).

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 >= 1 hours ).

Let's say the price updated by Chainlink is higher than the protocol's cached price. An attacker can front-run the protocol's oracle price update and execute the createBid() to place a Bid order with price == the protocol's cached price (stale price). Because of the condition: timeDiff >= 1 hours, the updateOracleAndStartingShortViaTimeBidOnly() will not trigger the _updateOracleAndStartingShort(). Therefore, the protocol's cached price will not be updated.

Once the _createBid() loads the oracle price (b.oraclePrice) from the cached storage, the stale price will be consumed.

Next, even though the _createBid() will execute the LibOrders::updateOracleAndStartingShortViaThreshold() to make sure that if the price difference between the 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 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 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.

Nevertheless, the bidMatchAlgo() also matches the 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 a price below the actual price. In other words, the minting mechanism will eventually catalyze the minted asset to depeg.

function createBid(
address asset,
uint80 price,
uint88 ercAmount,
bool isMarketOrder,
MTypes.OrderHint[] calldata orderHintArray,
uint16[] calldata shortHintArray
)
external
isNotFrozen(asset)
onlyValidAsset(asset)
nonReentrant
returns (uint88 ethFilled, uint88 ercAmountLeft)
{
// Update oracle and starting short if last updated more than 1 hour ago
@> LibOrders.updateOracleAndStartingShortViaTimeBidOnly( //@audit -- The createBid() executes the updateOracleAndStartingShortViaTimeBidOnly() with oracleFrequency == OF.OneHour that can be front-run not to trigger the _updateOracleAndStartingShort()
@> asset, OF.OneHour, shortHintArray
@> );
@> return _createBid( //@audit -- After the oracle update has been front-run, the createBid() executes the _createBid()
msg.sender,
asset,
price,
ercAmount,
isMarketOrder,
orderHintArray,
shortHintArray
);
}
...
function _createBid(
address sender,
address asset,
uint80 price,
uint88 ercAmount,
bool isMarketOrder,
MTypes.OrderHint[] memory orderHintArray,
uint16[] memory shortHintArray
) private returns (uint88 ethFilled, uint88 ercAmountLeft) {
...
MTypes.BidMatchAlgo memory b;
@> b.oraclePrice = LibOracle.getPrice(asset); //@audit -- The _createBid() loads the b.oraclePrice from the cached storage (LibOracle.getPrice(asset)); the oracle price is now stale
b.askId = s.asks[asset][Constants.HEAD].nextId;
//@dev setting initial shortId to match "backwards" (See _shortDirectionHandler() below)
b.shortHintId = b.shortId = Asset.startingShortId;
emit Events.CreateBid(asset, sender, incomingBid.id, incomingBid.creationTime);
STypes.Order memory lowestSell = _getLowestSell(asset, b);
if (
incomingBid.price >= lowestSell.price
&& (
lowestSell.orderType == O.LimitAsk || lowestSell.orderType == O.LimitShort
)
) {
//@dev if match and match price is gt .5% to saved oracle in either direction, update startingShortId
@> LibOrders.updateOracleAndStartingShortViaThreshold( //@audit -- The _createBid() executes the updateOracleAndStartingShortViaThreshold(), the attacker can manipulate the incomingBid.price to be within 0.5% price deviations not to trigger the _updateOracleAndStartingShort()
@> asset, b.oraclePrice, incomingBid, shortHintArray
@> );
b.shortHintId = b.shortId = Asset.startingShortId;
@> return bidMatchAlgo(asset, incomingBid, orderHintArray, b); //@audit -- After the b.oraclePrice's update has been front-run and the incomingBid.price has been manipulated, The _createBid() executes the bidMatchAlgo() to match the Bid order with Sell (both Ask and Short) orders whose prices are less than the actual price (Chainlink)
} else {
//@dev no match, add to market if limit order
LibOrders.addBid(asset, incomingBid, orderHintArray);
return (0, ercAmount);
}
}
  • The createBid() executes the updateOracleAndStartingShortViaTimeBidOnly() with oracleFrequency == OF.OneHour that can be front-run not to trigger the _updateOracleAndStartingShort(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L54-L56

  • After the oracle update has been front-run, the createBid() executes the _createBid(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L58

  • The _createBid() loads the b.oraclePrice from the cached storage (LibOracle.getPrice(asset)); the oracle price is now stale: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L129

  • The _createBid() executes the updateOracleAndStartingShortViaThreshold(), the attacker can manipulate the incomingBid.price to be within 0.5% price deviations not to trigger the _updateOracleAndStartingShort(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L144-L146

  • After the b.oraclePrice's update has been front-run and the incomingBid.price has been manipulated, The _createBid() executes the bidMatchAlgo() to match the Bid order with Sell (both Ask and Short) orders whose prices are less than the actual price (Chainlink): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/BidOrdersFacet.sol#L148

Impact

The createBid() 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 can profit by spending the least zETH to get the stable assets. 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.