DeFiHardhat
35,000 USDC
View results
Submission Details
Severity: low
Invalid

Protocol is going to ingest heavily outdated data

Proof of Concept

First take a look at https://github.com/Cyfrin/2024-04-beanstalk-2/blob/27ff8c87c9164c1fbff054be5f22e56f86cdf127/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L165-L177

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;
}

This function is called in all instances of querying prices from chainlink, whether it being an ETH/USD feed that has a 1 hour heartbeat, or any other price feed that could have as high as a day as their heartbeat.

Now consider https://github.com/Cyfrin/2024-04-beanstalk-2/blob/27ff8c87c9164c1fbff054be5f22e56f86cdf127/protocol/contracts/libraries/Oracle/LibWstethEthOracle.sol#L68-L98

function getWstethEthPrice(uint256 lookback) internal view returns (uint256 wstethEthPrice) {
//@audit
uint256 chainlinkPrice = lookback == 0 ?
LibChainlinkOracle.getPrice(WSTETH_ETH_CHAINLINK_PRICE_AGGREGATOR, LibChainlinkOracle.FOUR_DAY_TIMEOUT) :
LibChainlinkOracle.getTwap(WSTETH_ETH_CHAINLINK_PRICE_AGGREGATOR, LibChainlinkOracle.FOUR_DAY_TIMEOUT, lookback);
// Check if the chainlink price is broken or frozen.
if (chainlinkPrice == 0) return 0;
uint256 stethPerWsteth = IWsteth(C.WSTETH).stEthPerToken();
chainlinkPrice = chainlinkPrice.mul(stethPerWsteth).div(CHAINLINK_DENOMINATOR);
// Uniswap V3 only supports a uint32 lookback.
if (lookback > type(uint32).max) return 0;
uint256 uniswapPrice = LibUniswapOracle.getTwap(
lookback == 0 ? LibUniswapOracle.FIFTEEN_MINUTES :
uint32(lookback),
WSTETH_ETH_UNIV3_01_POOL, C.WSTETH, C.WETH, ONE
);
// Check if the uniswapPrice oracle fails.
if (uniswapPrice == 0) return 0;
if (LibOracleHelpers.getPercentDifference(chainlinkPrice, uniswapPrice) < MAX_DIFFERENCE) {
wstethEthPrice = chainlinkPrice.add(uniswapPrice).div(AVERAGE_DENOMINATOR);
if (wstethEthPrice > stethPerWsteth) wstethEthPrice = stethPerWsteth;
wstethEthPrice = wstethEthPrice.div(PRECISION_DENOMINATOR);
}
}

This function is used to get the WstethEth price and evidently it calls the LibChainlinkOracle and passes the argument to the getPrice/getTwap functions depending on if the lookback is attached or not, case however is that this function hardcodes the maxTimeout value to a whooping value of 345600.

Keep in mind that this is not the same with the previously accepted issue where protocol uses a hardcoded 4 hours for the Eth/Usd feed, protocol currently do not fix this and seem to conclude that this is an "accepted fault" this can be seen by navigating to the known accepted risk for protocol as stated during the contest, however this report pertains to a new contract in scope, i.e WstethEth.sol and also the new variable stated in the new LibChainlinkOracle.sol contract

uint256 constant FOUR_DAY_TIMEOUT = 345600;

So now back to this instance https://github.com/Cyfrin/2024-04-beanstalk-2/blob/27ff8c87c9164c1fbff054be5f22e56f86cdf127/protocol/contracts/libraries/Oracle/LibWstethEthOracle.sol#L68-L98 in the WstethEth.sol contract, we can see that protocol uses a hardcoded 345600 for the stale data check, whereas the WstethEth/Usd feed that's provided by chainlink and going to be used by protocol 0x86392dC19c0b719886221c78AB11eb8Cf5c52812 has a 1 day heartbeat... this setting now allows for heavy stale data usage, putting this in a layman's logic, a price from Thursday evening could be integrated to protocol the next week on a Monday morning/afternoon.

Impact

Protocol would ingest heavily outdated/stale prices, keep in mind that this is even more dangerous compare to the current accepted risk cause here we are talking about having protocol ingest price data that could be > 3.9 days old

Also keep in mind that according to the current state and the docs around the LibChainlinkOracle contract this setting can be seen to be the go to for all feeds that have a 1 day heartbeat, this can be seen here https://github.com/Cyfrin/2024-04-beanstalk-2/blob/27ff8c87c9164c1fbff054be5f22e56f86cdf127/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L25-L27

Recommended Mitigation Steps

Reconsider the logic of applying value for 345600 seconds as the timeout for Chainlink feeds that have their heartbeat set as 1 day

Updates

Lead Judging Commences

giovannidisiena Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

Chainlink timeout

Support

FAQs

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