DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Invalid

Improper Sequencer Status Validation Leading to Potential Price Manipulation

Summary

The _validatePrice function in the contract improperly determines the sequencer status by relying on a hardcoded condition (answer == 0). Additionally, it does not properly validate the startedAt timestamp, which can lead to bypassing the sequencer downtime check, causing stale or incorrect price feeds to be used. This creates a significant security risk, potentially enabling attackers to manipulate prices and cause incorrect liquidations or margin calculations.

Vulnerability Details

function _validatePrice(address perpVault, MarketPrices memory prices) internal view {
// L2 Sequencer check
(
/*uint80 roundID*/,
int256 answer,
uint256 startedAt,
/*uint256 updatedAt*/,
/*uint80 answeredInRound*/
) = AggregatorV2V3Interface(sequencerUptimeFeed).latestRoundData();
bool isSequencerUp = answer == 0; // ❌ Hardcoded check (incorrect validation)
require(isSequencerUp, "sequencer is down");
// ❌ Incorrect time check: startedAt may be 0 initially, leading to invalid calculations.
uint256 timeSinceUp = block.timestamp - startedAt;
require(timeSinceUp > GRACE_PERIOD_TIME, "Grace period is not over");
// Fetch market data
address market = IPerpetualVault(perpVault).market();
IVaultReader reader = IPerpetualVault(perpVault).vaultReader();
MarketProps memory marketData = reader.getMarket(market);
_check(marketData.indexToken, prices.indexTokenPrice.min);
_check(marketData.indexToken, prices.indexTokenPrice.max);
_check(marketData.longToken, prices.indexTokenPrice.min);
_check(marketData.longToken, prices.indexTokenPrice.max);
_check(marketData.shortToken, prices.shortTokenPrice.min);
_check(marketData.shortToken, prices.shortTokenPrice.max);
}

startedAt can be 0 when the round is first initialized, meaning block.timestamp - startedAt will return a very large value.

  • This can lead to the grace period check always passing, even if the sequencer status has not been properly updated.

Impact

If startedAt == 0, the contract might assume the sequencer is up, leading to a false sense of security and incorrect market price validation.

Tools Used

manual review

Recommendations

Instead of assuming answer == 0 means the sequencer is up, explicitly check both answer and startedAt validity:

function _validatePrice(address perpVault, MarketPrices memory prices) internal view {
(
/*uint80 roundID*/,
int256 answer,
uint256 startedAt,
/*uint256 updatedAt*/,
/*uint80 answeredInRound*/
) = AggregatorV2V3Interface(sequencerUptimeFeed).latestRoundData();
require(answer == 0, "sequencer is down"); // ✅ Ensure sequencer is marked as up
require(startedAt > 0, "invalid sequencer update"); // ✅ Ensure startedAt is properly updated
uint256 timeSinceUp = block.timestamp - startedAt;
require(timeSinceUp > GRACE_PERIOD_TIME, "Grace period is not over");
}
Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_sequencerUptimeFeed_startedAt_0_no_roundId

startedAt is only 0 when contract is not initialized on Arbitrum, but it is already initialized on Arbitrum. startedAt is sufficient for the protocol, it does not need roundID. Current documentation of Chainlink does not have this sentence: “This timestamp returns `0` if a round is invalid.“

Support

FAQs

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