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

Four-Day CHAINLINK_TIMEOUT in `LibWstethEthOracle::getWstethEthPrice` leads to inaccurate price data

Summary

The LibWstethEthOracle library utilizes a CHAINLINK_TIMEOUT constant set to 345,600 seconds (4 days). This duration is four times longer than the Chainlink heartbeat of 86,400 seconds (1 day), potentially introducing a significant delay in recognizing stale or outdated price data. Additionally, due to the implementation of the price retrieval mechanism in this library, it is possible that the returned wstethEthPrice often results in 0, due to deviation more than the allowed 1% tolerance from the returned TWAP price from Uniswap.

Vulnerability Details

Let's analyze the LibWstethEthOracle::getWstethEthPrice function to understand the source of this issue.

function getWstethEthPrice(uint256 lookback) internal view returns (uint256 wstethEthPrice) {
uint256 chainlinkPrice = lookback == 0
? LibChainlinkOracle.getPrice(
C.WSTETH_ETH_CHAINLINK_PRICE_AGGREGATOR,
@> LibChainlinkOracle.FOUR_DAY_TIMEOUT
)
: LibChainlinkOracle.getTwap(
C.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),
C.WSTETH_ETH_UNIV3_01_POOL,
C.WSTETH,
C.WETH,
ONE
);
// Check if the Uniswap price 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);
}
}

As we can see, when lookback is equal to 0, the protocol uses the LibChainlinkOracle.getPrice function with a 4-day timeout period. However, the STETH/ETH price feed's heartbeat is 1 day. This means that it is possible to receive a stale price when using the getWstethEthPrice function. A safeguard against this is the Uniswap's TWAP, ensuring that the price returned from Chainlink and Uniswap doesn't deviate more than 1%. However, due to querying a 4-day old price, it is very likely that the price deviates more than 1%, often resulting in the returned value from getWstethEthPrice being zero, as we will never enter the if statement and the returned value will be the default one for uint256.

Here is the check for 1% deviation, which will often not be entered due to the significant time gap between querying prices:

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 makes functions dependent on LibWstethEthOracle::getWstethEthPrice perform operations with a 0 value.

Impact

Functions that depend on LibWstethEthOracle::getWstethEthPrice may operate with zero value, leading to failed transactions or incorrect operations.

Tools Used

VSCode, manual code review

Recommendations

Consider reducing the CHAINLINK_TIMEOUT to align more closely with the Chainlink heartbeat on Ethereum to ensure the price data remains current and accurate.

Updates

Lead Judging Commences

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

Known - Bean Part 1

Appeal created

krisp Submitter
about 1 year ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

Known - Bean Part 1

Support

FAQs

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