Summary
getOraclePrice
function can return stale prices when oracle != baseOracle
because oracleCircuitBreaker
doesn't check if chainlinkPrice
and baseChainlinkPrice
prices are outdated.
Vulnerability Details
The issue occurs in the oracleCircuitBreaker
function below :
function oracleCircuitBreaker(
uint80 roundId,
uint80 baseRoundId,
int256 chainlinkPrice,
int256 baseChainlinkPrice,
uint256 timeStamp,
uint256 baseTimeStamp
) private view {
bool invalidFetchData = roundId == 0 ||
timeStamp == 0 ||
timeStamp > block.timestamp ||
chainlinkPrice <= 0 ||
baseRoundId == 0 ||
baseTimeStamp == 0 ||
baseTimeStamp > block.timestamp ||
baseChainlinkPrice <= 0;
if (invalidFetchData) revert Errors.InvalidPrice();
}
As you can see the function verifies many conditions on the input values but it doesn't check if returned prices are outdated or not by verifying that the price timestamp is not too old, this should have been done like in baseOracleCircuitBreaker
using : block.timestamp > 2 hours + timeStamp
.
Because oracleCircuitBreaker
doesn't verify timeStamp
and baseTimeStamp
, when getOraclePrice
is called and oracle != baseOracle
either basePrice
or price
(or both) could be outdated and thus the final the returned price priceInEth
could be outdated. This issue can impact all the protocol functions that rely on accurate price feed in their logic.
function getOraclePrice(address asset) internal view returns (uint256) {
AppStorage storage s = appStorage();
AggregatorV3Interface baseOracle = AggregatorV3Interface(s.baseOracle);
uint256 protocolPrice = getPrice(asset);
(
uint80 baseRoundID,
int256 basePrice,
,
uint256 baseTimeStamp,
) = baseOracle.latestRoundData();
AggregatorV3Interface oracle = AggregatorV3Interface(
s.asset[asset].oracle
);
if (address(oracle) == address(0)) revert Errors.InvalidAsset();
if (oracle == baseOracle) {
...
} else {
(
uint80 roundID,
int256 price,
,
uint256 timeStamp,
) = oracle.latestRoundData();
uint256 priceInEth = uint256(price).div(uint256(basePrice));
oracleCircuitBreaker(
roundID,
baseRoundID,
price,
basePrice,
timeStamp,
baseTimeStamp
);
return priceInEth;
}
}
Impact
The protocol functions that rely on accurate price feed might not work as expected and sometimes can lead to a loss of funds.
Tools Used
Manual review
Recommended Mitigation
Add staleness check in the oracleCircuitBreaker
function, it should be modified as follows :
function oracleCircuitBreaker(
uint80 roundId,
uint80 baseRoundId,
int256 chainlinkPrice,
int256 baseChainlinkPrice,
uint256 timeStamp,
uint256 baseTimeStamp
) private view {
bool invalidFetchData = roundId == 0 ||
timeStamp == 0 ||
timeStamp > block.timestamp ||
block.timestamp > 2 hours + timeStamp ||
chainlinkPrice <= 0 ||
baseRoundId == 0 ||
baseTimeStamp == 0 ||
baseTimeStamp > block.timestamp ||
block.timestamp > 2 hours + baseTimeStamp ||
baseChainlinkPrice <= 0;
if (invalidFetchData) revert Errors.InvalidPrice();
}