The Standard

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

Missing checks of the updatedAt timestamp returned by oracle against a threshold

Summary

No checks for the the updatedAt timestamp returned by Chainlink against a threshold can lead to outdated price being used.

Vulnerability Details

There is no checks for the time at which Chainlink returned the data. The returned data could be calculated too far behind from the moment that the latestRoundData() is called.
Since the price of oracle could vary in the time, this could lead to outdated price.

PriceCalculator::tokenToEurAvg(), PriceCalculator::tokenToEur() and PriceCalculator::eurToToken() fetches the asset price from a Chainlink aggregator using the latestRoundData function. However, there are no checks on timeStamp, resulting in stale prices. The oracle wrapper calls out to a Chainlink oracle receiving the latest round data. It then checks freshness by verifying that the answer is indeed for the last known round. The returned updatedAt timestamp is not checked.

Impact

Outdated/Stale data used.
Front runnable issue.

Tools Used

Manual review

Recommendations

Compare the updatedAt parameter returned from latestRoundData() to a threshold.

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();
+ if (roundTS < block.timestamp - THRESHOLD) revert("stale price feed");
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 updatedAt,) = clEurUsd.latestRoundData();
+ if (updatedAt < block.timestamp - THRESHOLD) revert("stale price feed");
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);
+ uint256 updatedPeriod = block.timestamp - THRESHOLD
- (,int256 _tokenUsdPrice,,,) = tokenUsdClFeed.latestRoundData();
+ (,int256 _tokenUsdPrice,, uint256 updatedAtTU,) = tokenUsdClFeed.latestRoundData();
+ if (updatedAtTU < updatedPeriod) revert("stale price feed");
uint256 collateralUsd = scaledCollateral * uint256(_tokenUsdPrice);
- (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
+ (, int256 eurUsdPrice,, uint256 updatedAtEU,) = clEurUsd.latestRoundData();
+ if (updatedAtEU < updatedPeriod) revert("stale price feed");
return collateralUsd / uint256(eurUsdPrice);
}
function eurToToken(ITokenManager.Token memory _token, uint256 _eurValue) external view returns (uint256) {
Chainlink.AggregatorV3Interface tokenUsdClFeed = Chainlink.AggregatorV3Interface(_token.clAddr);
+ uint256 updatedPeriod = block.timestamp - THRESHOLD
- (, int256 tokenUsdPrice,,,) = tokenUsdClFeed.latestRoundData();
+ (, int256 tokenUsdPrice,, uint256 updatedAtTU,) = tokenUsdClFeed.latestRoundData();
+ if (updatedAtTU < updatedPeriod) revert("stale price feed");
- (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
+ (, int256 eurUsdPrice,, uint256 updatedAtEU,) = clEurUsd.latestRoundData();
+ if (updatedAtEU < updatedPeriod) revert("stale price feed");
return _eurValue * uint256(eurUsdPrice) / uint256(tokenUsdPrice) / 10 ** getTokenScaleDiff(_token.symbol, _token.addr);
}

THRESHOLD should be the heartbeat of the oracle’s price feed which is 86400s or 1 hour in this case.

Updates

Lead Judging Commences

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

Chainlink-price

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

Chainlink-price

Support

FAQs

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