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

`stETH/ETH` chainlink oracle has too long of heartbeat and deviation threshold which can cause loss of funds

Summary

LibUsdOracle:getUsdPrice calls LibWstethUsdOracle:getWstethEthPrice which is stETH/ETH chainlink oracle, to calculate the current price of a given token in USD. This token valuation is used to determine the amount of stETH to skim from the user resulting from oracle arb. This is problematic since stETH/ETH has a 24 hour (1 day) heartbeat and a 2% deviation threshold. This deviation in price could easily cause loss of funds to the user. This protocol is not just using 1 day heartbeat, instead using 4 days heartbeat which is way worse.

Vulnerability Details

The following getWstethEthPrice function uses the stETH/ETH oracle to determine the price which as stated above has a 24 hour hearbeat and 2% deviation threshold (And beanstalk is using 4 days heartbeat), this means that the price can move up to 2% or 24 hours (even 4 days) before a price update is triggered. The result is that the on-chain price could be much different than the true stETH price.

function getWstethEthPrice(uint256 lookback) internal view returns (uint256 wstethEthPrice) {
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 price is used when determining how much stETH to send back to the user. Since the oracle can be up to 2% different from the true price, the user can unfairly lose part of their funds.

function getUsdPrice(address token, uint256 lookback) internal view returns (uint256) {
if (token == C.WETH) {
uint256 ethUsdPrice = LibEthUsdOracle.getEthUsdPrice(lookback);
if (ethUsdPrice == 0) return 0;
return uint256(1e24).div(ethUsdPrice);
}
if (token == C.WSTETH) {
uint256 wstethUsdPrice = LibWstethUsdOracle.getWstethUsdPrice(lookback);
if (wstethUsdPrice == 0) return 0;
return uint256(1e24).div(wstethUsdPrice);
}
revert("Oracle: Token not supported.");
}

Impact

User will be unfairly penalized due large variance between on-chain price and asset price. And the price of given token will also be not accurate as compared to market price.

Tools Used

Manual Review

Recommendations

Use the stETH/USD oracle instead because it has a 1-hour heartbeat and a 1% deviation threshold.

Updates

Lead Judging Commences

giovannidisiena Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

wstETH:ETH price calculation

0xbeastboy Submitter
over 1 year ago
giovannidisiena Lead Judge
over 1 year ago
giovannidisiena Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

wstETH:ETH price calculation

Support

FAQs

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