15,000 USDC
View results
Submission Details
Severity: medium
Valid

Missing L2 sequencer uptime check leads to consuming stale price data

Summary

The DSC protocol does not check the L2 Sequencer's uptime. If the protocol is deployed on L2 chains and the Sequencer goes down, the DSC protocol could consume stale price data, leading to exploitations.

Vulnerability Details

The DSC protocol is assumed to support every EVM chain (confirmed by the client), including L2 chains such as Arbitrum, Optimism, and Metis.

On the Arbitrum chain, for example, the so-called Sequencer is used for submitting batches of L2 transactions to the L1 chain. In case of Sequencer downtime, Chainlink will not be able to feed the fresh price data to the L2 chain. Therefore, the price data consumed by the DSC protocol could become stale.

However, an attacker can still manually submit their L2 messages via L1 into the delayed Inbox of the L2 network. Then, an attacker can execute their L2 messages through the Inbox's forceInclusion(). In this way, an attacker can execute L2 transactions to exploit the DSC protocol that is consuming the stale price data. For more info, please refer to this Chainlink's docs.

I noticed that the staleCheckLatestRoundData() does not check the Sequencer's uptime status. Therefore, the function could return stale price data.

function staleCheckLatestRoundData(AggregatorV3Interface priceFeed)
public
view
returns (uint80, int256, uint256, uint256, uint80)
{
(uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) =
priceFeed.latestRoundData();
uint256 secondsSince = block.timestamp - updatedAt;
if (secondsSince > TIMEOUT) revert OracleLib__StalePrice();
return (roundId, answer, startedAt, updatedAt, answeredInRound);
}

https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/libraries/OracleLib.sol#L21-L33

Impact

Even if the Sequencer goes down, the DSC protocol still allows users/attackers to execute its functions, such as mintDsc(), burnDsc(), redeemCollateral(), and liquidate(). However, the protocol could consume the stale price data unconsciously, leading to exploitations. This could result in the disruption of the protocol.

Tools Used

Manual Review

Recommendations

I recommend executing the revertIfSequencerInActive() in the staleCheckLatestRoundData(), as shown below. Specifically, the revertIfSequencerInActive() will revert if the Sequencer goes down and make sure that the grace period has passed after the Sequencer is back up.

- function staleCheckLatestRoundData(AggregatorV3Interface priceFeed)
+ function staleCheckLatestRoundData(
+ AggregatorV3Interface sequencerUptimeFeed,
+ AggregatorV3Interface priceFeed
+ )
public
view
returns (uint80, int256, uint256, uint256, uint80)
{
+ // This function will revert if the Sequencer is down or the grace period has not passed
+ revertIfSequencerInActive(sequencerUptimeFeed);
(uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) =
priceFeed.latestRoundData();
uint256 secondsSince = block.timestamp - updatedAt;
if (secondsSince > TIMEOUT) revert OracleLib__StalePrice();
return (roundId, answer, startedAt, updatedAt, answeredInRound);
}
+ // This function will revert if the Sequencer is down or the grace period has not passed
+ function revertIfSequencerInActive(AggregatorV3Interface sequencerUptimeFeed) public view {
+ (, int256 answer, uint256 startedAt, , ) = sequencerUptimeFeed.latestRoundData();
+
+ // answer == 0: Sequencer is up
+ // answer == 1: Sequencer is down
+ if (answer == 1) {
+ revert OracleLib__SequencerDown();
+ }
+
+ // Make sure the grace period has passed after the Sequencer is back up
+ uint256 timeSinceUp = block.timestamp - startedAt;
+ if (timeSinceUp <= GRACE_PERIOD_TIME) { //@audit -- e.g., GRACE_PERIOD_TIME = 3600
+ revert OracleLib__GracePeriodNotOver();
+ }
+ }

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.