DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: medium
Valid

USD prices dont work for 20 hours per day

Summary

LibUsdOracle will revert for stablecoins for 20 hours per day almost every day expect rare depeg events.

Vulnerability Details

function getTokenPriceFromExternal(
address token,
uint256 lookback
) internal view returns (uint256 tokenPrice) {
AppStorage storage s = LibAppStorage.diamondStorage();
.....
return
uint256(1e24).div(
LibChainlinkOracle.getTokenPrice(
chainlinkOraclePriceAddress,
LibChainlinkOracle.FOUR_HOUR_TIMEOUT,
///@audit ^ incorrect timeout
lookback
)
); ///@audit reverts here as division by 0
} else if (oracleImpl.encodeType == bytes1(0x02)) {
// assumes a dollar stablecoin is passed in
// if the encodeType is type 2, use a uniswap oracle implementation.
....
// USDC/USD
// call chainlink oracle from the OracleImplmentation contract
Implementation memory chainlinkOracleImpl = s.sys.oracleImplementation[chainlinkToken];
address chainlinkOraclePriceAddress = chainlinkOracleImpl.target;
.....
uint256 chainlinkTokenPrice = LibChainlinkOracle.getTokenPrice(
chainlinkOraclePriceAddress,
LibChainlinkOracle.FOUR_HOUR_TIMEOUT, ///@audit < same
lookback
);
return tokenPrice.mul(chainlinkTokenPrice).div(1e6);
}

Chainlink stables have a heart beat of 24 hours i.e if the deviation threshold is not passed for this much time there will be no update to the price , but getTokenPriceFromExternal() hardcodes this limit to 4 hours which leads to price of token = 0 if the token is a stablecoin

Here's a POC -

/**
* SPDX-License-Identifier: MIT
**/
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import {C} from "contracts/C.sol";
import {LibEthUsdOracle} from "../../../contracts/libraries/Oracle/LibEthUsdOracle.sol";
import {LibUniswapOracle} from "../../../contracts/libraries/Oracle/LibUniswapOracle.sol";
import {LibChainlinkOracle} from "../../../contracts/libraries/Oracle/LibChainlinkOracle.sol";
import {LibWstethUsdOracle} from "../../../contracts/libraries/Oracle/LibWstethUsdOracle.sol";
import "../../../contracts/libraries/Oracle/LibWstethEthOracle.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {LibAppStorage} from "contracts/libraries/LibAppStorage.sol";
import {Implementation} from "contracts/beanstalk/storage/System.sol";
import {AppStorage} from "contracts/beanstalk/storage/AppStorage.sol";
import {LibRedundantMath256} from "contracts/libraries/LibRedundantMath256.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {console} from "forge-std/console.sol";
import {LibUsdOracle} from "../../../contracts/libraries/Oracle/LibUsdOracle.sol";
interface IERC20Decimals {
function decimals() external view returns (uint8);
}
interface ChainlinkPriceFeedRegistry {
function getFeed(address base, address quote) external view returns (address aggregator);
}
contract Diamond{
AppStorage internal s;
}
contract PriceTesterWstethETH is Diamond,Test{
address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address public constant DAIUSDC = 0x6c6Bc977E13Df9b0de53b251522280BB72383700;
address public constant USDCUSD = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6;
address public constant AAVEUSD = 0x547a514d5e3769680Ce22B2361c10Ea13619e8a9;
address public constant AAVE = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9;
using LibRedundantMath256 for uint256;
function setUp() public {
vm.label(DAI,"DAI");
vm.label(USDC,"USDC");
vm.label(DAIUSDC,"UniV3DaiUsdc");
vm.label(USDCUSD,"USDCFeed");
vm.label(AAVEUSD,"AAVEUSD");
vm.createSelectFork("https://rpc.ankr.com/eth",20008200);
// vm.createSelectFork("https://rpc.ankr.com/eth",20253304 +5);
// for the above fork the execution does not revert
}
function testPrice2() public {
bytes4 b;
Implementation memory a2 = Implementation({
target: USDCUSD,
selector: b,
encodeType:bytes1(0x01)
});
s.sys.oracleImplementation[USDC] = a2;
uint price2;
// Execution reverts due to LibChainlinkOracle returning 0 & 1e24/0 happening
price2 = LibUsdOracle.getUsdPrice(USDC);
emit log_named_decimal_uint("price2", price2, 6);
}
}

The claims can also be verified by looking at
https://data.chain.link/feeds/base/base/usdt-usd
&
https://data.chain.link/feeds/ethereum/mainnet/usdc-usd

where libUsdOracle is setting maxTimeout = 4 hours but the chainlink feeds only get updated once per day in most of the cases(strictly stablecoins) .
You can also verify the same by uncommenting the 2nd fork line USDC/USD was updated in 20253304 block number so then it will work but in blocks after that i.e for rest of 20 hours for the day it wont.
As it wont pass legitimacy checks in LibChainlinkOracle.sol
Run the Poc by putting it in test/foundry/OracleLibs(newDir)/Filename.t.sol for working imports

Please note that the POC reverts in the first fork due to division of 1e24/ tokenPrice( which is 0 due to timeout)

Code snippets-
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/4e0ad0b964f74a1b4880114f4dd5b339bc69cd3e/protocol/contracts/libraries/Oracle/LibUsdOracle.sol#L100-L161

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/4e0ad0b964f74a1b4880114f4dd5b339bc69cd3e/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol#L182-L196

Impact

Non functioning price feeds , breaks core functionality

Tools Used

Manual Review

Recommendations

For stables set threshold(maxTmeout) of 24 hours.

Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Using 4 hour heartbeat for USDC/USD chainlink price feed which has a 24 h heartbeat

Support

FAQs

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