DeFiHardhatOracleProxyUpdates
100,000 USDC
View results
Submission Details
Severity: low
Valid

In `getEthUsdPrice` & `getEthUsdTwap` ChainlinkAdapterOracle will return minPrice if price crashes

Summary

LibChainlinkOracle:getEthUsdPrice & LibChainlinkOracle:getEthUsdTwap functions use latestRoundData to get price of ETH/USD. ChainlinkFeedRegistry#latestRoundData pulls the associated aggregator and requests round data from it. ChainlinkAggregators have minPrice and maxPrice circuit breakers built into them. This means that if the price of the asset drops below the minPrice, the protocol will continue to value the token at minPrice instead of it's actual value.

In LibEthUsdOracle:getEthUsdPrice the ETH/USD price from Chainlink Oracle is also compared with ETH/USDC & ETH/USDT from Uniswap. The logic is if ETH/USD & ETH/USDC price difference is within 0.3% range then an average of these two will be returned otherwise it will do the same for ETH/USDT. If none of these is true then it will just return ETH/USD price form chainlink.

Now if price crashes hard and chainlink returns minPrice, it will obviously not satisfy both checking conditions with Uniswap. The oracle price will be returned, which will be the wrong price as compared to market.
Example:
TokenA has a minPrice of $1. The price of TokenA drops to $0.10. The aggregator still returns $1 allowing the user to borrow against TokenA as if it is $1 which is 10x it's actual value.

Vulnerability Details

See the code below:

function getEthUsdPrice() internal view returns (uint256 price) {
// First, try to get current decimal precision:
uint8 decimals;
try priceAggregator.decimals() returns (uint8 _decimals) {
// If call to Chainlink succeeds, record the current decimal precision
decimals = _decimals;
} catch {
// If call to Chainlink aggregator reverts, return a price of 0 indicating failure
return 0;
}
// Secondly, try to get latest price data:
try priceAggregator.latestRoundData() returns (
uint80 roundId,
int256 answer,
uint256 /* startedAt */,
uint256 timestamp,
uint80 /* answeredInRound */
) {
// Check for an invalid roundId that is 0
if (roundId == 0) return 0;
if (checkForInvalidTimestampOrAnswer(timestamp, answer, block.timestamp)) {
return 0;
}
// Adjust to 6 decimal precision.
return uint256(answer).mul(PRECISION).div(10 ** decimals);
} catch {
// If call to Chainlink aggregator reverts, return a price of 0 indicating failure
return 0;
}
}
/**
* @dev Returns the TWAP ETH/USD price from the Chainlink Oracle over the past `lookback` seconds.
* Return value has 6 decimal precision.
* Returns 0 if Chainlink's price feed is broken or frozen.
**/
function getEthUsdTwap(uint256 lookback) internal view returns (uint256 price) {
// First, try to get current decimal precision:
uint8 decimals;
try priceAggregator.decimals() returns (uint8 _decimals) {
// If call to Chainlink succeeds, record the current decimal precision
decimals = _decimals;
} catch {
// If call to Chainlink aggregator reverts, return a price of 0 indicating failure
return 0;
}
// Secondly, try to get latest price data:
try priceAggregator.latestRoundData() returns (
uint80 roundId,
int256 answer,
uint256 /* startedAt */,
uint256 timestamp,
uint80 /* answeredInRound */
) {
// Check for an invalid roundId that is 0
if (roundId == 0) return 0;
if (checkForInvalidTimestampOrAnswer(timestamp, answer, block.timestamp)) {
return 0;
}
uint256 endTimestamp = block.timestamp.sub(lookback);
// Check if last round was more than `lookback` ago.
if (timestamp <= endTimestamp) {
return uint256(answer).mul(PRECISION).div(10 ** decimals);
} else {
uint256 cumulativePrice;
uint256 lastTimestamp = block.timestamp;
// Loop through previous rounds and compute cumulative sum until
// a round at least `lookback` seconds ago is reached.
while (timestamp > endTimestamp) {
cumulativePrice = cumulativePrice.add(
uint256(answer).mul(lastTimestamp.sub(timestamp))
);
roundId -= 1;
try priceAggregator.getRoundData(roundId) returns (
uint80 /* roundId */,
int256 _answer,
uint256 /* startedAt */,
uint256 _timestamp,
uint80 /* answeredInRound */
) {
if (checkForInvalidTimestampOrAnswer(_timestamp, _answer, timestamp)) {
return 0;
}
lastTimestamp = timestamp;
timestamp = _timestamp;
answer = _answer;
} catch {
// If call to Chainlink aggregator reverts, return a price of 0 indicating failure
return 0;
}
}
cumulativePrice = cumulativePrice.add(
uint256(answer).mul(lastTimestamp.sub(endTimestamp))
);
return cumulativePrice.mul(PRECISION).div(10 ** decimals).div(lookback);
}
} catch {
// If call to Chainlink aggregator reverts, return a price of 0 indicating failure
return 0;
}
}

Impact

In the event that an asset crashes (i.e. LUNA) the protocol can be manipulated to give out loans at an inflated price. This can open doors for several attacks on the protocol to loose assets.

Tools Used

Manual Review

Recommendations

ChainlinkAdapterOracle should check the returned answer against the minPrice/maxPrice and revert if the answer is outside of the bounds:

Updates

Lead Judging Commences

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

Oracle min price

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:

Oracle min price

Support

FAQs

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