Solidity Attack Vectors: #10 Insufficient Gas Griefing

Insufficient gas griefing occurs when a contract makes one or more external function calls and does not check for return values or whether the external function call was successful. In this type of attack, the attacker provides a small amount of gas to execute only the logic inside a function, but not enough gas for any external calls that the function may make. If the contract does not have measures in place to check if the external function call was successful, it will continue as if everything is fine.

For example, consider the following contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GriefingRelayer {
    Target target;
    mapping(bytes => bool) executed;

    constructor(address _target) {
        target = Target(_target);
    }

    function execute(bytes memory _value) external {
        require(!executed[_value], "Duplicate call");
        executed[_value] = true;

        address(target).call(abi.encodeWithSignature("execute(bytes)", _value));
    }
}

contract Target {
    uint256[] values;

    function execute(bytes memory _value) external returns (bool) {
        for (uint i; i < _value.length; i++) {
            values.push(i + 1);
        }

        return true;
    }
}

The "execute" function in the "GriefingRelayer" contract accepts a bytes argument, sets the bytes argument to true in the "executed" mapping, and then makes an external call to the "Target" contract without checking if it was successful. An attacker could provide a very small amount of gas, enough to set the bytes execution status to true but not enough to make the external call. However, because this is not being checked, the contract will simply continue as if everything is fine. In this case, the attack is not particularly beneficial to the attacker, but rather causes grief for the contract owner, as there will be a bunch of "executed" bytes that were not actually executed.

How Can This Be Prevented?

To prevent this type of attack, developers can:

  1. Check the return values of external calls, if any, and ensure they are what is expected.
  2. Estimate the gas cost to run the entire function and the external call(s) and use a "require" statement to check that the gas supplied is sufficient to run them.

Conclusion

It is important to ensure that the defined logic of a contract is fully executed and that any external calls are successful. Failing to do so can lead to unexpected behavior and potential griefing attacks. As developers, it is our responsibility to thoroughly test and secure our contracts to prevent these types of vulnerabilities.