15,000 USDC
View results
Submission Details
Severity: high
Valid

Inaccurate Calculations Due to Varying Token Decimal Lengths

Summary

The contract performs several calculations related to token amounts and their USD values, and if the tokens have varying decimal lengths, it can lead to incorrect results.

The issue arises due to the difference in token precision caused by varying decimal lengths. For example, if one token has 18 decimal places (like most ERC-20 tokens) and another token has fewer decimal places, say 6, performing direct arithmetic operations between them could result in precision loss and inaccurate calculations.

For instance, when converting token amounts to USD values or calculating ratios, the contract relies on mathematical operations that may not consider the varying decimal lengths, leading to potential miscalculations.

Vulnerability Details

The Chainlink USD price feeds return values with 8 decimal places represented as int256.

priceFeed data from the ETH/USD pair

The DSCEngine contract fails to consider tokens with decimal places other than 18 when calculating collateral values and the health factor. This oversight poses potential risks, especially when dealing with tokens with varying decimal lengths. Here are instances where this issue occurs:

Calculating values of tokens

function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) public view returns (uint256) {
// price of ETH (token)
// $/ETH ETH ??
// $2000 / ETH. $1000 = 0.5 ETH
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
// ($10e18 * 1e18) / ($2000e8 * 1e10)
return (usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION);
}

This function is used to convert USD values to token amounts for a given token. However, the calculation assumes that both USD and token values have 18 decimal places, which may not be true for all tokens. If tokens with a different number of decimals are involved, this calculation can lead to inaccurate results.

function getUsdValue(address token, uint256 amount) public view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
// 1 ETH = $1000
// The returned value from CL will be 1000 * 1e8
return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION;
}

This function calculates the USD value of a given token amount using the token's price obtained from an external Chainlink price feed. However, similar to the previously mentioned functions, the calculation assumes that both the token price and the token amount have 18 decimal places. This can lead to incorrect USD value calculations for tokens with a different number of decimals.

Calculating collateral value

function getAccountCollateralValue(address user) public view returns (uint256 totalCollateralValueInUsd) {
// loop through each collateral token, get the amount they have deposited, and map it to
// the price, to get the USD value
for (uint256 i = 0; i < s_collateralTokens.length; i++) {
address token = s_collateralTokens[i];
uint256 amount = s_collateralDeposited[user][token];
totalCollateralValueInUsd += getUsdValue(token, amount);
}
return totalCollateralValueInUsd;
}

This function calculates the total collateral value in USD for a specific user by looping through each collateral token deposited by the user. However, the calculation may not account for tokens with different decimal lengths, potentially leading to incorrect total collateral value.

Calculating health factor

function _healthFactor(address user) private view returns (uint256) {
(uint256 totalDscMinted, uint256 collateralValueInUsd) = _getAccountInformation(user);
return _calculateHealthFactor(totalDscMinted, collateralValueInUsd);
}
function _calculateHealthFactor(uint256 totalDscMinted, uint256 collateralValueInUsd)
internal
pure
returns (uint256)
{
if (totalDscMinted == 0) return type(uint256).max;
uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION;
return (collateralAdjustedForThreshold * 1e18) / totalDscMinted;
}

The vulnerability revolves around the health factor calculation performed by the _healthFactor() and _calculateHealthFactor() functions. These functions are crucial for determining the financial health of a user's account within the DSCEngine contract, particularly when assessing whether the account is at risk of liquidation.

The vulnerability arises because the health factor calculation does not account for tokens with different decimal lengths. Both functions assume that all token values, including the collateral and DSC amounts, have 18 decimal places. However, some tokens may have a different number of decimals, leading to potential inaccuracies in the health factor calculation.

Liquidation

function liquidate(address collateral, address user, uint256 debtToCover)
external
moreThanZero(debtToCover)
nonReentrant
{
// need to check health factor of the user
uint256 startingUserHealthFactor = _healthFactor(user);
if (startingUserHealthFactor >= MIN_HEALTH_FACTOR) {
revert DSCEngine__HealthFactorOk();
}
uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION;
uint256 totalCollateralToRedeem = tokenAmountFromDebtCovered + bonusCollateral;
_redeemCollateral(user, msg.sender, collateral, totalCollateralToRedeem);
// We need to burn the DSC
_burnDsc(debtToCover, user, msg.sender);
uint256 endingUserHealthFactor = _healthFactor(user);
if (endingUserHealthFactor <= startingUserHealthFactor) {
revert DSCEngine__HealthFactorNotImproved();
}
_revertIfHealthFactorIsBroken(msg.sender);
}

The liquidate function involves calculations related to debt, collateral value, and liquidation bonus, which may not consider the varying decimal lengths of tokens involved. This can result in incorrect liquidation outcomes.

Impact

❌Loss of funds

The vulnerability in the DSCEngine contract, arising from the failure to account for tokens with different decimal lengths, can have significant consequences on the system's stability and security. The inaccurate health factor calculations may lead to improper decisions regarding minting, collateralization, and liquidation of DSC tokens, potentially resulting in financial losses, system instability, and an increased risk of liquidation events. Addressing this vulnerability is crucial to ensure accurate and reliable assessments of user account health, maintaining the stability and integrity of the decentralized stablecoin system. Below are different events that may occur:

Different Decimal Lengths for Collateral and DSC Tokens

User A has deposited 10.5 tokens of "TokenX" as collateral, where "TokenX" has 6 decimal places.
User A has minted 500 DSC tokens, where DSC has 18 decimal places.
The contract calculates the collateral value in USD by using getUsdValue(token, amount) and assumes both the collateral and DSC have 18 decimal places.
Since "TokenX" has 6 decimal places, the calculated collateral value will be 10.5 USD instead of the correct value.
As a result, the health factor will be inaccurately calculated, potentially leading to incorrect decisions regarding liquidation or minting, and posing risks to the stability of the system.

Incorrect Health Factor Leading to Liquidation

User B has deposited 1000 "TokenY" as collateral, where "TokenY" has 8 decimal places.
User B has minted 1000 DSC tokens, where DSC has 18 decimal places.
The contract calculates the health factor using _calculateHealthFactor(totalDscMinted, collateralValueInUsd).
Due to the different decimal lengths, the health factor is incorrectly computed, leading the contract to believe that User B's account is sufficiently collateralized.
In reality, User B's collateral value might be below the liquidation threshold, putting the system at risk if it does not take the correct liquidation actions.

System Instability and Losses

Multiple users with tokens of different decimal lengths participate in the system.
The health factor calculation consistently fails to account for the varying decimal lengths.
The inaccurate health factor calculations lead to improper assessments of user account health, resulting in over-minting or under-collateralization of DSC tokens.
Over time, this can lead to system instability, potential losses, and a higher likelihood of liquidation events.

Tools Used

VSCode, Foundry

Recommendations

To ensure accurate calculations in the contract, it's essential to handle token amounts and conversions correctly, taking into account the different decimal lengths. This can be achieved by normalizing token amounts to a common precision level, like 18 decimal places, before performing any arithmetic operations involving different tokens. Additionally, using libraries or built-in functions specifically designed for handling token arithmetic can help prevent precision loss and ensure accurate calculations regardless of the token decimal lengths.

Implementing proper handling of token amounts with varying decimal lengths is crucial to maintain the integrity of the contract's functionality and prevent potential calculation errors. Below is an example of how this can be mitigated:

  1. If a token has 6 decimal places, multiply its amount by 1e12 (10^12) to adjust it to 18 decimal places.

  2. If a token has 8 decimal places, multiply its amount by 1e10 (10^10) to adjust it to 18 decimal places.

Support

FAQs

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