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.