The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

PriceCalculation results are untrustable and do not scale decimals correctly

Although it appears to be out of scope, I do want to mention the PriceCalculator contract is not handling Oracle data correctly. Because the SmartVault highly depends on these results, I still think it's important to mention this issue.

I've combined both issues in the PriceCalculator contract. Please mitigate these issues

1. Oracle price data

All methods don't validate the Oracle return value suffficiently

avgPrice
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/utils/PriceCalculator.sol#L23
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/utils/PriceCalculator.sol#L29
tokenToEurAvg
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/utils/PriceCalculator.sol#L47
tokenToEur
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/utils/PriceCalculator.sol#L54
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/utils/PriceCalculator.sol#L56
eurToToken
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/utils/PriceCalculator.sol#L62-L63

Vulnerability details

There is no validation to check if the answer (or price) received was actually a stale one. Reasons for a price feed to stop updating are listed here. Using a stale price in the application can result in wrong calculations.

https://docs.chain.link/data-feeds/api-reference

Impact

According to Chainlink's documentation, This function does not error if no answer has been reached but returns 0, causing an incorrect price fed. The external Chainlink oracle, which provides index price information to the system, introduces risk inherent to any dependency on third-party data sources. For example, the oracle could fall behind or otherwise fail to be maintained, resulting in outdated data being fed to the index price calculations of the liquidity.

The SmartVault uses the PriceCalculator contract 5 times, making it highly dependent on a correct implementation of the contract.

It directly calculated the collateral of the vault, if the price is not correctly validated this can result in an undercollateralized vault and results in liquidation.

Tools Used

Manual Review

Recommended mitigation steps

Everytime the PriceCalculator fetch data from the Chainlink Oracle, it is recommended to validate if the answer/price is correctly, and to check for stale prices. This can be achieved by doing the following:

(,int256 answer,, uint256 updatedAt,) = Chainlink.AggregatorV3Interface(aggregatorAddress).latestRoundData();
if (answer <= 0) {
revert("invalid price data");
}
if (updatedAt < block.timestamp - 60 * 60 /* 1 hour */) {
revert("Stale Price");
}

2. Scaling token decimals

The getTokenScaleDiff assumes all ERC20 tokens have the decimals() getter, but that's not true.
For example the IOTA token does not have this method implemented. This results in a reverted transaction if this token is going to be supported. But just as IOTA there are more tokens not implementing the decimals method. So assuming the method is always present can resulting in DOS fetching the token price.

Another issue that can arise is what if a token has more than 1e18 decimals? This results in a negative value in the getTokenScaleDiff, also resulting in reverting the transaction.

Vulnerability details

Following the Chainlink documentation they give the following example

If you require a denomination other than what is provided, you can use two data feeds to derive the pair that you need. For example, if you needed a BTC / EUR price, you could take the BTC / USD feed and the EUR / USD feed and derive BTC / EUR using division.

$ \frac{BTC/USD}{EUR/USD} = BTC/EUR $

This is exactly what happens in tokenToEur.

Chainlink uses 8 decimals unless it's an ETH pair.

Impact

Not scaling to the right amount of decimals can result in wrong accounting in collateral or token prices. Because Vaults can get liquidated it is important these prices and decimals are correctly used.

Tools Used

Manual Review

Recommended mitigation steps

It is recommended to use the Aggregator decimals method and scale correctly.

Add the following two methods inside the PriceCalculation smart contracts

// https://docs.chain.link/data-feeds/using-data-feeds#getting-a-different-price-denomination
function scalePrice(
int256 _price,
uint8 _priceDecimals,
uint8 _decimals
) internal pure returns (int256) {
if (_priceDecimals < _decimals) {
return _price * int256(10 ** uint256(_decimals - _priceDecimals));
} else if (_priceDecimals > _decimals) {
return _price / int256(10 ** uint256(_priceDecimals - _decimals));
}
return _price;
}
/**
* Network: Sepolia
* Base: BTC/USD
* Base Address: 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
* Quote: EUR/USD
* Quote Address: 0x1a81afB8146aeFfCFc5E50e8479e826E7D55b910
* Decimals: 8
*/
/**
* https://docs.chain.link/data-feeds/using-data-feeds#getting-a-different-price-denomination
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
function getDerivedPrice(
address _base,
address _quote,
uint8 _decimals
) public view returns (int256) {
require(
_decimals > uint8(0) && _decimals <= uint8(18),
"Invalid _decimals"
);
int256 decimals = int256(10 ** uint256(_decimals)); // decimals from token
(, int256 basePrice, , , ) = Chainlink.AggregatorV3Interface(_base)
.latestRoundData();
uint8 baseDecimals = Chainlink.AggregatorV3Interface(_base).decimals();
basePrice = scalePrice(basePrice, baseDecimals, _decimals);
(, int256 quotePrice, , , ) = Chainlink.AggregatorV3Interface(_quote)
.latestRoundData();
uint8 quoteDecimals = Chainlink.AggregatorV3Interface(_quote).decimals();
quotePrice = scalePrice(quotePrice, quoteDecimals, _decimals);
return (basePrice * decimals) / quotePrice;
}
function tokenToEur(ITokenManager.Token memory _token, uint256 _tokenValue) external view returns (uint256) {
Chainlink.AggregatorV3Interface tokenUsdClFeed = Chainlink.AggregatorV3Interface(_token.clAddr);
- uint256 scaledCollateral = _tokenValue * 10 ** getTokenScaleDiff(_token.symbol, _token.addr);
- (,int256 _tokenUsdPrice,,,) = tokenUsdClFeed.latestRoundData();
- uint256 collateralUsd = scaledCollateral * uint256(_tokenUsdPrice);
- (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
- return collateralUsd / uint256(eurUsdPrice);
return getDerivedPrice(tokenUsdClFeed, clEurUsd, 8); // for non ETH pairs, 18 for ETH pairs
}
Updates

Lead Judging Commences

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

Chainlink-price

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

Chainlink-price

Support

FAQs

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