Summary
When computing the price of a stablecoin by using a uniswap v3 twap implementation, it will result in a DoS for most of the time
Relevant GitHub Links:
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/main/protocol/contracts/libraries/Oracle/LibUsdOracle.sol#L155-L159
Vulnerability Details
If we look at the getTokenPriceFromExternal
function we can see there are 3 different scenarios according to the encodeType we pass in:
function getTokenPriceFromExternal(
address token,
uint256 lookback
) internal view returns (uint256 tokenPrice) {
AppStorage storage s = LibAppStorage.diamondStorage();
Implementation memory oracleImpl = s.sys.oracleImplementation[token];
if (oracleImpl.encodeType == bytes1(0x01)) {
...
fetches chainlink price feed
...
} else if (oracleImpl.encodeType == bytes1(0x02)) {
address chainlinkToken = IUniswapV3PoolImmutables(oracleImpl.target).token0();
chainlinkToken = chainlinkToken == token
? IUniswapV3PoolImmutables(oracleImpl.target).token1()
: token;
tokenPrice = LibUniswapOracle.getTwap(
lookback == 0 ? LibUniswapOracle.FIFTEEN_MINUTES : uint32(lookback),
oracleImpl.target,
chainlinkToken,
token,
uint128(10) ** uint128(IERC20Decimals(token).decimals())
);
Implementation memory chainlinkOracleImpl = s.sys.oracleImplementation[chainlinkToken];
address chainlinkOraclePriceAddress = chainlinkOracleImpl.target;
if (chainlinkOraclePriceAddress == address(0)) {
chainlinkOraclePriceAddress = ChainlinkPriceFeedRegistry(chainlinkRegistry).getFeed(
chainlinkToken,
0x0000000000000000000000000000000000000348
);
}
uint256 chainlinkTokenPrice = LibChainlinkOracle.getTokenPrice(
chainlinkOraclePriceAddress,
LibChainlinkOracle.FOUR_HOUR_TIMEOUT,
lookback
);
return tokenPrice.mul(chainlinkTokenPrice).div(1e6);
}
address target = oracleImpl.target;
...
third implementation
...
}
If the encodeType is equal to bytes1(0x01), it fetches the price of the token using a chainlink price feed.
If the encodeType is equal to bytes1(0x02), it is assumed that a stablecoin is passed (according to the comments) and it fetches the twap from uniswap v3. Afterwards, it fetches the price of USDC/USD from a chainlink price feed. However, to do that it passes a 4-hour timeout that will make the result to be most of the time 0 because the heartbeat of the USDC/USD chainlink price feed is 24 hours, so it will exceed the 4 hour timeout and will understand that the price is stale. In this case, the timeout passed should be the one that is compatible with the 24-hour heartbeat price feeds that is stored in the LibChainlinkOracle
file:
uint256 constant FOUR_DAY_TIMEOUT = 345600;
Impact
High
Every price of any token that would be configured to be extracted from a uniswap v3 oracle, will not work for 20 hours each day due to the timeout.
Tools Used
Manual review
Recommendations
Change the timeout to be 4 days instead of 4 hours:
...
uint256 chainlinkTokenPrice = LibChainlinkOracle.getTokenPrice(
chainlinkOraclePriceAddress,
- LibChainlinkOracle.FOUR_HOUR_TIMEOUT,
+ LibChainlinkOracle.FOUR_DAY_TIMEOUT,
lookback
);
...