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.
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.
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.
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);
}