The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

Missing checks for active whether Arbitrum Sequencer in PriceCalculator

Summary

Chainlink recommends that Arbitrum L2 oracles consult the Sequencer Uptime Feed to ensure that the sequencer is live before trusting the data returned by the oracle. This check is missing in the PriceCalculator contract.

Vulnerability Details

The protocol has been deployed on Arbitrum L2.
If the Arbitrum Sequencer goes down, oracle data will not be kept up to date, and thus could become stale. However, as usual, users will be able to interact with the protocol directly through the Arbitrum L1 rollup contract. See Chainlink documentation on L2 Sequencer Uptime Feeds for more details.

Since users will be able to interact with while oracle feeds are stale. This could cause many problems, a malicious user can take advantage of the downtime.
Consider a scenario where a malicious user called Bob has borrowed Euros at price x of PAXG. Then the Arbitrum sequencer goes down temporarily. While it's down, the price of the Euros falls below the price x to a price y of PAXG. Now Bob swaps his to PAXG and call SmartVaultV3::removeCollateral() to withdraw his PAXGs. The amount of PAXG Bob remove will be above the amount he should remove. The protocol will send more PAXG to Bob.

Impact

If the Arbitrum sequencer goes down, the protocol will allow users to continue to operate at the previous (stale) rates. Which can lead to protocol losing funds.

Tools Used

Manual review

Recommendations

Use sequencer oracle to determine whether the sequencer is offline or not, and don't allow orders to be executed while the sequencer is offline or if the sequencer has been running for too little long.

File: contracts/utils/PriceCalculator.sol
function avgPrice(uint8 _hours, Chainlink.AggregatorV3Interface _priceFeed) private view returns (uint256) {
uint256 startPeriod = block.timestamp - _hours * 1 hours;
uint256 roundTS;
uint80 roundId;
int256 answer;
- (roundId, answer,, roundTS,) = _priceFeed.latestRoundData();
+ (roundId, answer, startedAt, roundTS,) = _priceFeed.latestRoundData();
+ if (answer == 0) revert("sequencer down");
+ if (block.timestamp - startedAt <= GRACE_PERIOD_TIME) revert("grace period not over");
uint256 accummulatedRoundPrices = uint256(answer);
uint256 roundCount = 1;
while (roundTS > startPeriod && roundId > 1) {
roundId--;
try _priceFeed.getRoundData(roundId) {
(, answer,, roundTS,) = _priceFeed.getRoundData(roundId);
accummulatedRoundPrices += uint256(answer);
roundCount++;
} catch {
continue;
}
}
return accummulatedRoundPrices / roundCount;
}
function tokenToEurAvg(ITokenManager.Token memory _token, uint256 _tokenValue) external view returns (uint256) {
Chainlink.AggregatorV3Interface tokenUsdClFeed = Chainlink.AggregatorV3Interface(_token.clAddr);
uint256 scaledCollateral = _tokenValue * 10 ** getTokenScaleDiff(_token.symbol, _token.addr);
uint256 collateralUsd = scaledCollateral * avgPrice(4, tokenUsdClFeed);
- (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
+ (, int256 eurUsdPrice, uint256 startedAt,,,) = clEurUsd.latestRoundData();
+ if (eurUsdPrice == 0) revert("sequencer down");
+ if (block.timestamp - startedAt <= GRACE_PERIOD_TIME) revert("grace period not over");
return collateralUsd / uint256(eurUsdPrice);
}
function tokenToEur(ITokenManager.Token memory _token, uint256 _tokenValue) external view returns (uint256) {
Chainlink.AggregatorV3Interface tokenUsdClFeed = Chainlink.AggregatorV3Interface(_token.clAddr);
uint256 scaledCollateral = _tokenValue * 10 ** getTokenScaleDiff(_token.symbol, _token.addr);
- (,int256 _tokenUsdPrice,,,) = tokenUsdClFeed.latestRoundData();
+ (,int256 _tokenUsdPrice, uint256 startedAtTU,,,) = tokenUsdClFeed.latestRoundData();
+ if (!_tokenUsdPrice == 0) revert("sequencer down");
+ if (block.timestamp - startedAtTU <= GRACE_PERIOD_TIME) revert("grace period not over");
uint256 collateralUsd = scaledCollateral * uint256(_tokenUsdPrice);
- (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
+ (, int256 eurUsdPrice, uint256 startedAtEU,,,) = clEurUsd.latestRoundData();
+ if (eurUsdPrice == 0) revert("sequencer down");
+ if (block.timestamp - startedAtEU <= GRACE_PERIOD_TIME) revert("grace period not over");
return collateralUsd / uint256(eurUsdPrice);
}
function eurToToken(ITokenManager.Token memory _token, uint256 _eurValue) external view returns (uint256) {
Chainlink.AggregatorV3Interface tokenUsdClFeed = Chainlink.AggregatorV3Interface(_token.clAddr);
- (, int256 tokenUsdPrice,,,) = tokenUsdClFeed.latestRoundData();
+ (, int256 tokenUsdPrice, uint256 startedAtTU,,,) = tokenUsdClFeed.latestRoundData();
+ if (eurUsdPrice == 0) revert("sequencer down");
+ if (block.timestamp - startedAtTU <= GRACE_PERIOD_TIME) revert("stale price feed");
- (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
+ (, int256 eurUsdPrice, uint256 startedAtEU,,,) = clEurUsd.latestRoundData();
+ if (eurUsdPrice == 0) revert("sequencer down");
+ if (block.timestamp - startedAtEU <= GRACE_PERIOD_TIME) revert("stale price feed");
return _eurValue * uint256(eurUsdPrice) / uint256(tokenUsdPrice) / 10 ** getTokenScaleDiff(_token.symbol, _token.addr);
}
Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Arbitrum-sequncer

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

Arbitrum-sequncer

Support

FAQs

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