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

LibUsdOracle inverses Chainlink TWAP price which results in incorrect price

Summary

Here is good article on why arithmetic mean TWAP cannot be inversed to get price of second asset:
https://blog.yacademy.dev/2024-05-24-are-inverse-TWAP-prices-inaccurate/

In short inverse of average price is not equal to average of inverse prices. The more volatile the price over the observed period, the greater the distortion will be.
That's why Uniswap V2 stores 2 prices: token0 and token1 to calculate each TWAP. But that's true only for arithmetic mean, in Uni V3 geometric mean is used, and this price can be inversed.

Vulnerability Details

LibUsdOracle.sol returns price USD / Token by inversing price fetched from ordinary oracle:

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);
}
// 1e18 * 1e6 = 1e24.
uint256 tokenPrice = getTokenPriceFromExternal(token, lookback);
if (tokenPrice == 0) return 0;
@> return uint256(1e24).div(tokenPrice);
}

One of oracle sources is Chainlink TWAP like in LibEthUsdOracle.getEthUsdPrice():

function getEthUsdPrice(uint256 lookback) internal view returns (uint256) {
return
lookback > 0
? LibChainlinkOracle.getTwap(
C.ETH_USD_CHAINLINK_PRICE_AGGREGATOR,
LibChainlinkOracle.FOUR_HOUR_TIMEOUT,
lookback
)
: LibChainlinkOracle.getPrice(
C.ETH_USD_CHAINLINK_PRICE_AGGREGATOR,
LibChainlinkOracle.FOUR_HOUR_TIMEOUT
);
}

And finally let's see that Chainlink TWAP calculates arithmetic mean TWAP:

function getTwap(
address priceAggregatorAddress,
uint256 maxTimeout,
uint256 lookback
) internal view returns (uint256 price) {
...
// Secondly, try to get latest price data:
try priceAggregator.latestRoundData() returns (
uint80 roundId,
int256 answer,
uint256 /* startedAt */,
uint256 timestamp,
uint80 /* answeredInRound */
) {
...
while (timestamp > t.endTimestamp) {
@> t.cumulativePrice = t.cumulativePrice.add(
uint256(answer).mul(t.lastTimestamp.sub(timestamp))
);
roundId -= 1;
t.lastTimestamp = timestamp;
(answer, timestamp) = getRoundData(priceAggregator, roundId);
if (
checkForInvalidTimestampOrAnswer(
timestamp,
answer,
t.lastTimestamp,
maxTimeout
)
) {
return 0;
}
}
@> t.cumulativePrice = t.cumulativePrice.add(
uint256(answer).mul(t.lastTimestamp.sub(t.endTimestamp))
);
return t.cumulativePrice.mul(PRECISION).div(10 ** decimals).div(lookback);
}
} catch {
...
}

Impact

LibUsdOracle.sol returns incorrect price if underlying oracle is Chainlink TWAP.

Tools Used

Manual Review

Recommendations

Do not inverse Chainlink TWAP price, instead calculate TWAP from inversed prices. However it requires refactor of oracle libraries.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

LibUsdOracle inverses Chainlink TWAP price which results in incorrect price

Appeal created

T1MOH Submitter
about 1 year ago
T1MOH Submitter
about 1 year ago
inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

LibUsdOracle inverses Chainlink TWAP price which results in incorrect price

Support

FAQs

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