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

The preset PRECISION of 1e18 in DSCEngine contract is not compatible with all collateral tokens (eg. wBTC), potentially causing loss of fund

Summary

The preset PRECISION of 1e18 in DSCEngine contract is not compatible with all collateral tokens (eg. wBTC), potentially causing loss of fund

Vulnerability Details

There are two functions within the contract affected by this PRECISION configuration.

[1]. In DSCEngine.sol:347, the getTokenAmountFromUsd function in DSCEngine.sol incorrectly calculates the expected wBTC amount where the returned amount is not properly derived due to a hardcoded precision value. Specifically, the precision value PRECISION is preset as 1e18, which works fine with wETH but not with wBTC. The wBTC.decimals function returns only 8 as can be seen here: https://etherscan.io/token/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599#readContract#F4.
This results in the returned wBTC token amount being scaled up by 1e10 which drastically overvalues the usdAmountInWei.

The vulnerable code snippet is as follows:

function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) public view returns (uint256) {
...
return (usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION);
}

[2]. The getUsdValue function also wrongly computes the value in USD of wBTC token due to the hardcoded PRECISION which makes the returned USD value 1e10 times smaller than expected. As a result, the user's wBTC collateral becomes much cheaper than its real value rendering an under-collateralized situation.

The vulnerable code snippet is as follows:

function getUsdValue(address token, uint256 amount) public view returns (uint256) {
...
return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION;
}

Impact

From the reasoning from [1] & [2], the user's wBTC collateral could be drained by just a tiny amount of the debt-cover DSC.
For instance, a user, who has a health factor less than 200% collateralized by wETH and is facing the risk of liquidation.

  • Bad User: $1400 in ETH, $1000 DSC -> health factor: 1400e18 * 50% * 1e18 / 1000e18 = 0.7e18

He decides to increase the health factor by depositing a significant amount of wBTC.

function _calculateHealthFactor(uint256 totalDscMinted, uint256 collateralValueInUsd) ...
{
...
uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION;
return (collateralAdjustedForThreshold * 1e18) / totalDscMinted;
}
  • Bad User: +1 BTC ~ 29500e18 in DSC -> under-collateralized issue causing BTC value to drop 1e10 times -> 29500e8 wei in DSC accounted -> new health factor: (1400e18 + 29500e8) * 50% * 1e18 / 1000e18 = 0.700000001475e18 (almost unchanged)

Since the heath factor remains unhealthy, a malicious actor carries out the exploit by calling DSCEngine.liquidate on a user's wBTC collateral with a dust amount of DSC tokens.

  • Bad User: $1400 in ETH, 1 BTC, $1000 - $debtToCover DSC

--> due to the overvalued usdAmountInWei, $debtToCover value could be manifested to match the totalCollateralToRedeem value of 1 BTC.

function liquidate(address collateral, address user, uint256 debtToCover)
external
moreThanZero(debtToCover)
nonReentrant
{
uint256 startingUserHealthFactor = _healthFactor(user);
if (startingUserHealthFactor >= MIN_HEALTH_FACTOR) {
revert DSCEngine__HealthFactorOk();
}
uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover);
uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / LIQUIDATION_PRECISION;
uint256 totalCollateralToRedeem = tokenAmountFromDebtCovered + bonusCollateral;
...
uint256 endingUserHealthFactor = _healthFactor(user);
if (endingUserHealthFactor <= startingUserHealthFactor) {
revert DSCEngine__HealthFactorNotImproved();
}
_revertIfHealthFactorIsBroken(msg.sender);
}

The exact numbers could be deduced from above function:

  • startingUserHealthFactor = 0.700000001475e18 (as computed above)

  • tokenAmountFromDebtCovered = (debtToCoverInWei * 1e18) / (29500e8 * 1e10) = debtToCoverInWei / 29500

  • bonusCollateral = debtToCoverInWei * 10% / 29500

  • totalCollateralToRedeem = debtToCoverInWei * 110% / 29500 = 1e8 BTC

=> debtToCoverInWei = 29500 * 1e8 / 110% = 268e10 DSC

  • endingUserHealthFactor = 1400e18 * 50% * 1e18 / (1000e18 - 268e10) = 0.700000001876e18

Since endingUserHealthFactor > startingUserHealthFactor and _revertIfHealthFactorIsBroken(msg.sender) checks for attacker's health factor which is irrelevant, the attacker can successfully liquidate user's debt. This proved that the actor caused 1 BTC in loss for user with just 268e10 wei DSC or $0.00000268 DSC.

Tools Used

Manual Review

Recommendations

To properly scale the collateral amount, it is recommended to call IERC20(token).decimals to obtain the token's decimal value and adjust the precision accordingly.

function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) public view returns (uint256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
(, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
- return (usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION);
+ return (usdAmountInWei * 10 ** IERC20(token).decimals()) / (uint256(price) * ADDITIONAL_FEED_PRECISION);
}
function getUsdValue(address token, uint256 amount) public view returns (uint256) {
...
- return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION;
+ return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / (10 ** IERC20(token).decimals());
}

Support

FAQs

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