medium

Double-spending vulnerability leads to a disruption of the DSC token

Reward

Total

442.18 USDC

47.04 USDC
47.04 USDC
47.04 USDC
47.04 USDC
47.04 USDC
47.04 USDC
Selected
65.86 USDC
47.04 USDC
47.04 USDC
Selected Submission

Double-spending vulnerability leads to a disruption of the DSC token

Severity

High Risk

Relevant GitHub Links

https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L120

https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L353-L357

Summary

There is a double-spending vulnerability in the DSCEngine contract, leading to a disruption of the DSC token.

Vulnerability Details

While constructing the DSCEngine contract, the whitelisted collateral tokens will be registered along with their corresponding price feed addresses. However, the registration process does not verify that a token cannot be registered twice.

For instance, assume that the ETH address is inputted in the array tokenAddresses twice, the ETH address will also be pushed into the array s_collateralTokens twice.

    constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {
        // USD Price Feeds
        if (tokenAddresses.length != priceFeedAddresses.length) {
            revert DSCEngine__TokenAddressesAndPriceFeedAddressesMustBeSameLength();
        }
        // For example ETH / USD, BTC / USD, MKR / USD, etc
        for (uint256 i = 0; i < tokenAddresses.length; i++) {
            s_priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];
@>          s_collateralTokens.push(tokenAddresses[i]);
        }
        i_dsc = DecentralizedStableCoin(dscAddress);
    }

https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L120

Subsequently, when the contract executes the getAccountCollateralValue() to compute users' collateral value, the function will process on the ETH address twice. In other words, if a user/attacker deposits 10 ETH as collateral, the getAccountCollateralValue() will return 20 ETH (in USD value), leading to a double-spending issue.

    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;
    }

https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L353-L357

Impact

With this double-spending vulnerability, an attacker can deposit ETH to double their collateral value and then mint DSC tokens over the limit (breaking the protocol's health factor invariant).

As a result, the DSCEngine contract will eventually be insolvent, and the DSC token will then be depegged to $0.

Tools Used

Manual Review

Recommendations

To fix the vulnerability, I recommend adding the require(s_priceFeeds[tokenAddresses[i]] == address(0), "Collateral token was already set"); to guarantee that there could not be any token ever registered twice.

    constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {
        // USD Price Feeds
        if (tokenAddresses.length != priceFeedAddresses.length) {
            revert DSCEngine__TokenAddressesAndPriceFeedAddressesMustBeSameLength();
        }
        // For example ETH / USD, BTC / USD, MKR / USD, etc
        for (uint256 i = 0; i < tokenAddresses.length; i++) {
+           require(s_priceFeeds[tokenAddresses[i]] == address(0), "Collateral token was already set");
            s_priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];
            s_collateralTokens.push(tokenAddresses[i]);
        }
        i_dsc = DecentralizedStableCoin(dscAddress);
    }