The LibChainlinkOracle#getTwap() function in the Beanstalk protocol would revert for valid TWAP queries due to the incorrect assumption that latestRoundData and getRoundData should be treated equally. This results in potential reverts when calculating TWAP using historical data in valid ranges that are not supposed to revert.
Take a look at https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L95-L164
This function is used to returns the TWAP price from the Chainlink Oracle over the past lookback seconds, issue as hinted by the @audit tag it makes a query to getRoundData to get the specific data for that round, and the implementation of this function is here https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L165-L180
Now, going to the official documentation by Chainlink, we can see that these are the Return values of the getRoundData call :
roundId: The round ID
answer: The answer for this round
startedAt: Timestamp of when the round started
updatedAt: Timestamp of when the round was updated
answeredInRound: Deprecated - Previously used when answers could take multiple rounds to be
Now would be key to note that the logic here unlike with latestRoundData where as the name suggests we should expect the updatedAt to not be far in the past, in the case of getRoundData for previous rounds, the updatedAt parameter can only be the last timestamp of when the round was updated.
Problem however with this integration is the fact that when checking both the call to latestRoundData & getRoundData the checkForInvalidTimestampOrAnswer functionality is being queried, as can be seen here, here & here, evidently, all these snippets are attaching the same maxTimeout to the calls, now where as that would be valid for instantaneous attempt at getting prices, for example let's use the LibEthUsdOracle as an example, we can see that from here the maxTimeour is set as the FOUR_HOUR_TIMEOUT https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/libraries/Oracle/LibEthUsdOracle.sol#L39-L45
This would be correct considering it's an attempt to get the instantaneous price, however from the same LibEthUsdOracle, we can see that this is how the twap price is being gotten https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/libraries/Oracle/LibEthUsdOracle.sol#L53-L65
Evidently, even if lookback > 0 the maxTimeout is the FOUR_HOUR_TIMEOUT but this would cause reverts in the @audit hinted part of the code snippet below, considering in the case of getting twap prices the updatedAT for these past rounds are also timestamps in the past
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L182-L196
LibChainlinkOracle#getTwap() would be broken in some cases since the same timeout value ism used for data received from the previous rounds, which would have their updatedAt timestamps in the past and as such have this block if (currentTimestamp.sub(timestamp) > maxTimeout) return true; triggered.
NB: This part is the direct snippet that has the last updated at time for the round to be checked with the current timestamp: https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L132-L158.
Manual review
Consider not having the same timeout check for the data gotten from the rounds and also latestRoundData . Instead, depending on how far back lookback is, increment the timeout value too.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.