The Standard

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

Chainlink's `latestRoundData` might return stale or incorrect results

Summary

In the LiquidationPool::distributeAssets() function, the Chainlink's latestRoundData() is used to retrieve price feed data of two different assets. However, there is insufficient protection against price staleness.

Return arguments other than int256 answer are necessary to determine the validity of the returned price, as it is possible for an outdated price to be received. The return value updatedAt contains the timestamp at which the received price was last updated, and can be used to ensure that the price is not outdated. See more information about latestRoundID in the Chainlink docs. Inaccurate price data can lead to functions not working as expected and/or loss of funds.

Instances:

  • https://github.com/Cyfrin/2023-12-the-standard/blob/91132936cb09ef9bf82f38ab1106346e2ad60f91/contracts/LiquidationPool.sol#L207

  • https://github.com/Cyfrin/2023-12-the-standard/blob/91132936cb09ef9bf82f38ab1106346e2ad60f91/contracts/LiquidationPool.sol#L218

Vulnerability Details

Observe the code below and notice that the only saved value from the latestRoundData() calls is the price of the asset (int answer of the AggregatorV3Interface interface):

@> (,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
uint256 stakeTotal = getStakeTotal();
uint256 burnEuros;
uint256 nativePurchased;
for (uint256 j = 0; j < holders.length; j++) {
Position memory _position = positions[holders[j]];
uint256 _positionStake = stake(_position);
if (_positionStake > 0) {
for (uint256 i = 0; i < _assets.length; i++) {
ILiquidationPoolManager.Asset memory asset = _assets[i];
if (asset.amount > 0) {
@> (,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();

For more information, see similar issues from other audits:

  • https://github.com/code-423n4/2023-06-stader-findings/issues/15

  • https://github.com/sherlock-audit/2023-04-hubble-exchange-judging/issues/18

Impact

The distributeAssets() function of the LiquidationPool contract would accept the use of stale prices for the assets in use.

Tools Used

Manual analysis.

Recommendations

Consider reading the updatedAt return value from the latestRoundData() function and verify that is not older than a specific time tolerance.

For example:

- (,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
+ (, int256 priceEurUsd, , uint256 updatedAt, ) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
+ require(block.timestamp - updatedAt <= MAX_DELAY, "Stale price");

and

- (,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
+ (, int256 assetPriceUsd, , uint256 updatedAt, ) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
+ require(block.timestamp - updatedAt <= MAX_DELAY, "Stale price");

Some other solutions found online also make use of the latest returned argument, answeredInRound. However, it was excluded from this recommendation because the latest Chainlink docs indicate that it is deprecated.

Updates

Lead Judging Commences

hrishibhat Lead Judge almost 2 years 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

EloiManuel Submitter
over 1 year ago

Support

FAQs

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