Denial-of-Service Attack caused by gas limit

From WEB3 Vulnerapedia
Jump to navigation Jump to search

Denial-of-service (DoS) attack caused by gas limit typically targets blockchain networks, especially those using Ethereum or similar platforms where smart contracts execute operations based on gas fees. Gas is the fuel that powers transactions and smart contract executions on these networks, and each operation consumes a certain amount of gas. The gas limit is the maximum amount of gas that can be consumed by a transaction or smart contract execution.

Denial of Service — Caused by gas limit

userDepositIndex and userWithdrawIndex growing indefinitely

The contract is using userDepositsIndex to track deposits and withdrawals for users.

/// @dev array of deposit receipts
Receipt[] public deposits;
/// @dev indexes of deposit receipts of an address
mapping(address => uint256[]) public userDepositsIndex;

userDepositsIndex is expanded by depositUSDC function:

function depositUSDC(uint256 _amount) external {
    require(_amount >= minUSDCAmount, "deposit amount smaller than minimum OTC amount");

    IERC20(usdc).transferFrom(msg.sender, address(this), _amount);

    // update usd balance of user, add their receipt, and receipt index to user deposits index
    usdBalance[msg.sender] = usdBalance[msg.sender] + _amount;
    deposits.push(Receipt(msg.sender, _amount));
    userDepositsIndex[msg.sender].push(deposits.length - 1);

    emit USDCQueued(msg.sender, _amount, usdBalance[msg.sender], deposits.length - 1);
}

The state variable userDepositIndex is growing indefinitely and might lead to expensive transactions and effectively a denial of service for the user

userDepositsIndex[msg.sender].push(deposits.length - 1);


When calling withdrawUSCD function that requires iterations over the whole array.

function withdrawUSDC(uint256 _amount) external {
    require(!isAuctionLive, "auction is live");

    usdBalance[msg.sender] = usdBalance[msg.sender] - _amount;
    require(
        usdBalance[msg.sender] >= minUSDCAmount || usdBalance[msg.sender] == 0,
        "remaining amount smaller than minimum, consider removing full balance"
    );

    // start withdrawing from the users last deposit
    uint256 toRemove = _amount;
    uint256 lastIndexP1 = userDepositsIndex[msg.sender].length;
    for (uint256 i = lastIndexP1; i > 0; i--) {
        Receipt storage r = deposits[userDepositsIndex[msg.sender][i - 1]];
        if (r.amount > toRemove) {
            r.amount -= toRemove;
            toRemove = 0;
            break;
        } else {
            toRemove -= r.amount;
            delete deposits[userDepositsIndex[msg.sender][i - 1]];
        }
    }
    IERC20(usdc).transfer(msg.sender, _amount);

    emit USDCDeQueued(msg.sender, _amount, usdBalance[msg.sender]);
}

part of the function that interests us:

uint256 toRemove = _amount;
uint256 lastIndexP1 = userDepositsIndex[msg.sender].length;
for (uint256 i = lastIndexP1; i > 0; i--) {
    Receipt storage r = deposits[userDepositsIndex[msg.sender][i - 1]];
    if (r.amount > toRemove) {
        r.amount -= toRemove;
        toRemove = 0;
        break;
    } else {
        toRemove -= r.amount;
        delete deposits[userDepositsIndex[msg.sender][i - 1]];
    }
}

And more specifically, it is in the loop that checks from an ever growing index to zero.

uint256 lastIndexP1 = userDepositsIndex[msg.sender].length;
for (uint256 i = lastIndexP1; i > 0; i--) {
  [...]

Recommendation

To avoid this it is recommended to remove elements from the arrays userDepositsIndex/userWithdrawsIndex using pop() function when deleting deposits.

Sources

https://medium.com/@bloqarl/uncovering-real-life-examples-of-denial-of-service-attacks-on-smart-contracts-8bc220c2cdd0