DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: low
Valid

In `ChainlinkUtil::getPrice` function, skipping `roundId` check at all can be disastrous

Summary

In ChainlinkUtil::getPrice function, the roundId is not checked or validated at all. Stale prices could result in mass liquidations and huge bad debts that could even challenge the going concern of the protocol.

Vulnerability Details

Zaros protocol is solely relying on Chainlink for most of the critical functionalities of the protocol. This actually introduces the chances of single point of failure. For off-chain price feeds, Zaros is using Chainlink's aggregator only.

While getting the off-chain price of an asset using ChainlinkUtil::getPrice function, the roundId is not checked or validated at all. However, updatedAt is compared with the priceFeedHeartbeatSeconds, but this sole check is not enough. As we can see in this report how important the validation of roundId is.

This another report is also suitable for our case because the protocol has used the updatedAt and some sort of stalePriceDelay in order to accept only the fresh prices and avoid any stale prices. The reason that this report is valid because roundId is not validated at all, that makes this logic vulnerable to disastrous exploits.

The Chainlink docs also emphasize the validation of roundId. Using the roundId we can ensure the data freshness, sequential data integrity, stale data detection and round completion.

Among others, a devastating example and lesson as a result of Chainlink price oracle malfunction we have TERRA LUNA. We have to implement as many controls as we can to avoid such mishaps.

Source: ChainlinkUtil.sol#L59C1-L76C6

...
try priceFeed.latestRoundData() returns (uint80, int256 answer, uint256, uint256 updatedAt, uint80) {
// no roundId validation <= FOUND
@> if (block.timestamp - updatedAt > priceFeedHeartbeatSeconds) {
revert Errors.OraclePriceFeedHeartbeat(address(priceFeed));
}
IOffchainAggregator aggregator = IOffchainAggregator(priceFeed.aggregator());
int192 minAnswer = aggregator.minAnswer();
int192 maxAnswer = aggregator.maxAnswer();
if (answer <= minAnswer || answer >= maxAnswer) {
revert Errors.OraclePriceFeedOutOfRange(address(priceFeed));
}
price = ud60x18(answer.toUint256() * 10 ** (Constants.SYSTEM_DECIMALS - priceDecimals));
} catch {
revert Errors.InvalidOracleReturn();
}
...

Impact

As an impact of wrong price feeds, positions may be liquidated prematurely or fail to liquidate when they should and eventually resulting bad debts. Positions open on wrong collateral prices which may cost major financial losses to the protocol. Incorrect collateral valuation can cause under-collateralization.

Tools Used

Manual review

Recommendations

To mitigate the risk, latestRoundData function returns the answeredInRound value among other returned values. Although this value is depreacated as mentioned in the docs but still this can be used to validate the roundId. If Zaros don't want to validate the roundId using answeredInRound (deprecated) then atleast it must check the startedAt timestamp because each roundData has this prop. The implication should be like, startedAt should be greater than last startedAt this will ensure that price does not pertain to previous round and the price is fresh and udpated.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

0xe4669da Submitter
about 1 year ago
0xe4669da Submitter
about 1 year ago
inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

In `ChainlinkUtil::getPrice` function, skipping `roundId` check

Support

FAQs

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