DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: medium
Invalid

LibChainlinkOracle will return incorrect TWAP price if token hits 0 price in any round

Summary

TWAP is Time Weighted Average Price. Let's look on example:
Suppose price = 5 for 5 seconds; price = 0 for 5 seconds; price = 10 for 5 seconds. As a result TWAP is (5 * 5 + 0 * 5 + 10 * 5) / 15 = 5

However current implementation will return 0 price if any of the observed rounds contained 0 price.

Vulnerability Details

In TWAP function it loops thorogh previous rounds and fetches data. Note that it calls checkForInvalidTimestampOrAnswer() every time to validate retrieved data and in case of error immediately returns 0:

function getTwap(
address priceAggregatorAddress,
uint256 maxTimeout,
uint256 lookback
) internal view returns (uint256 price) {
...
// Secondly, try to get latest price data:
try priceAggregator.latestRoundData() returns (
uint80 roundId,
int256 answer,
uint256 /* startedAt */,
uint256 timestamp,
uint80 /* answeredInRound */
) {
// Check for an invalid roundId that is 0
if (roundId == 0) return 0;
@> if (checkForInvalidTimestampOrAnswer(timestamp, answer, block.timestamp, maxTimeout)) {
return 0;
}
TwapVariables memory t;
t.endTimestamp = block.timestamp.sub(lookback);
// Check if last round was more than `lookback` ago.
if (timestamp <= t.endTimestamp) {
return uint256(answer).mul(PRECISION).div(10 ** decimals);
} else {
t.lastTimestamp = block.timestamp;
// Loop through previous rounds and compute cumulative sum until
// a round at least `lookback` seconds ago is reached.
while (timestamp > t.endTimestamp) {
t.cumulativePrice = t.cumulativePrice.add(
uint256(answer).mul(t.lastTimestamp.sub(timestamp))
);
roundId -= 1;
t.lastTimestamp = timestamp;
(answer, timestamp) = getRoundData(priceAggregator, roundId);
if (
@> checkForInvalidTimestampOrAnswer(
timestamp,
answer,
t.lastTimestamp,
maxTimeout
)
) {
return 0;
}
}
t.cumulativePrice = t.cumulativePrice.add(
uint256(answer).mul(t.lastTimestamp.sub(t.endTimestamp))
);
return t.cumulativePrice.mul(PRECISION).div(10 ** decimals).div(lookback);
}
} catch {
// If call to Chainlink aggregator reverts, return a price of 0 indicating failure
return 0;
}
}

Here you can see that it will signall an error if returned price is 0 assuming theer is an error on Chainlink side:

function checkForInvalidTimestampOrAnswer(
uint256 timestamp,
int256 answer,
uint256 currentTimestamp,
uint256 maxTimeout
) private pure returns (bool) {
// Check for an invalid timeStamp that is 0, or in the future
if (timestamp == 0 || timestamp > currentTimestamp) return true;
// Check if Chainlink's price feed has timed out
if (currentTimestamp.sub(timestamp) > maxTimeout) return true;
// Check for non-positive price
@> if (answer <= 0) return true;
return false;
}

Impact

TWAP incorrectly prices tokens if any of the observed rounds had 0 price.

Chainlink team confirmed that Price feed will report price 0 if it's fair price is 0, 0 answer is not error anymore. Here's answer:

image

Here is link to answer in Chainlink's Discord server

Tools Used

Manual Review

Recommendations

Refactor check:

function checkForInvalidTimestampOrAnswer(
uint256 timestamp,
int256 answer,
uint256 currentTimestamp,
uint256 maxTimeout
) private pure returns (bool) {
...
- if (answer <= 0) return true;
+ if (answer < 0) return true;
return false;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

T1MOH Submitter
11 months ago
inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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