When developing smart contracts that forward calls to other contracts, gas management becomes a critical security concern. One particularly subtle vulnerability is the “insufficient gas griefing attack,” where a malicious actor can manipulate gas forwarding to cause subcalls to fail while the main transaction succeeds. This article explores this vulnerability and presents a solution for it.

The Problem: Insufficient Gas Griefing

Consider a relayer (like an off-chain service) or proxy contract that forwards calls to other contracts on behalf of users. This pattern is common in:

  • Meta-transactions
  • Relayer networks
  • Multi-signature wallets
  • Contract upgradability patterns
  • Cross-chain messaging systems

The vulnerability arises when a malicious actor intentionally provides just enough gas for the main transaction to complete but insufficient gas for the forwarded call to succeed. This creates several problematic scenarios:

  • Silent Failures: The main transaction succeeds, but the intended operation fails
  • State Inconsistency: Partial execution can leave contract state in an inconsistent state
  • Economic Attacks: Attackers can manipulate transaction outcomes while paying minimal gas fees
  • Denial of Service: Valid transactions appear to succeed but don’t accomplish their intended purpose

Let’s examine a naive implementation that is vulnerable to this attack:

// VULNERABLE IMPLEMENTATION - DO NOT USE
function forwardCall(address target, bytes memory data) external {
    // No gas checks - vulnerable to insufficient gas griefing
    (bool success, ) = target.call(data);
    // enough gas to complete the call
    require(success, "Call failed");
}

In this implementation, an attacker could provide just enough gas for the forwarding contract to execute, but not enough for the target contract to complete its operations.

Naive gas left check

One common approach to prevent insufficient gas griefing is to check how much gas is left right before the subcall:

// UNSAFE GAS CHECK - DO NOT USE
function forwardCall(address target, bytes memory data, uint256 gasLimit) external {
    // Verify that sufficient gas is available
    require(gasleft() >= gasLimit, "Insufficient gas");
    // Make the call with specified gas
    (bool success, ) = target.call{gas: gasLimit}(data);
}

While this approach seems reasonable, it’s not secure.

The problem is that gasleft() returns the gas available at the time of the check and does not take the dynamic gas costs of the call operation into account.

This means that an attacker can still manipulate the gas forwarding to their advantage.

The EVM Gas Stipend Mechanism

To understand the solution, we need to understand how Ethereum’s EVM handles gas forwarding. When a contract calls another contract:

  • The EVM reserves 1/64th of the remaining gas for operations after the call returns
  • At most 63/64ths of the available gas is forwarded to the called contract
  • If the called contract consumes all forwarded gas, it reverts with an out-of-gas error
  • The calling contract continues execution with the reserved 1/64th gas

This 63/64 rule is crucial for developing a secure gas checking mechanism.

The Solution

Instead of trying to predict gas needs beforehand (which is nearly impossible due to the dynamic nature of EVM execution and the dynamic gas costs of the call opcode), they verify adequate gas was provided after the subcall completes.

Here’s an implementation:

// UNSAFE GAS CHECK - DO NOT USE
function forwardCall(address target, bytes memory data, uint256 gasLimit) external {
    // Make the call with specified gas
    (bool success, ) = target.call{gas: gasLimit}(data);

    require(gasleft() >= gasLimit / 63, "Error");
}

The Mathematical Insight

The brilliance of this solution lies in the mathematical relationship between the requested gas and the gas remaining after the call:

  • Let X be the gas available before the call, such that the call gets at most X * 63 / 64 (see EIP150).
    • X = gasLeftBeforeCall - callCosts
    • Since the gas costs for the call opcode are dynamic, we can’t know X, but what we want is: X * 63 / 64 >= forwardedGas.
  • Let Y be the gas available after the call: Y = gasLeftBeforeCall - callCosts - forwardedGas
  • This leads to: Y = X - forwardedGas
    • Y + forwardedGas = X
  • Since X * 63 / 64 >= forwardedGas and therefore X >= (forwardedGas * 64) / 63, we get:
    • Y + ForwardedGas >= (forwardedGas * 64) / 63
    • Y >= (forwardedGas * 64) / 63 - forwardedGas
    • Y >= forwardedGas / 63

Uff, that was a lot of math! Now, by checking if gasleft() >= forwardedGas / 63, we’re effectively verifying:

  • A relayer or proxy contract forwards to little gas to the target contract, the verification will fail
    • We can punish the relayer or just revert the transaction
  • If the the subcall failed due to running out of gas and the verification holds, we know the target contract used more gas than was forwarded
    • The relayer can’t be blamed for this and maybe even rewarded for doing their job correctly

Implementing Secure Gas Forwarding

Here’s a complete, secure implementation of a gas-aware forwarding contract:

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

contract SecureForwarder {
    event CallForwarded(address indexed target, bool success, uint256 gasUsed);
    
    struct ForwardRequest {
        address target;
        bytes data;
        uint256 gasLimit;
    }
    
    function forward(ForwardRequest calldata request) external {    
        // Make the call with specified gas
        (bool success, ) = request.target.call{gas: request.gasLimit}(request.data);
        // use require to check
        require(gasleft() >= request.gasLimit / 63, "Error");
    }

    function forward2(ForwardRequest calldata request) external {
        // Make the call with specified gas
        (bool success, ) = request.target.call{gas: request.gasLimit}(request.data);
        uint256 gasLeftAfter = gasleft();
        // use a function to check
        _checkForwardedGas(gasLeftAfter, request);
    }

    function _checkForwardedGas(uint256 gasLeft, ForwardRequest calldata request) private pure {
        if (gasLeft < request.gasLimit / 63) {
            assembly ("memory-safe") {
                invalid()
            }
        }

}

Conclusion

Gas forwarding is a critical aspect of many smart contract designs, but it can introduce subtle vulnerabilities if not handled correctly. The insufficient gas griefing attack is a prime example of how gas management can be exploited to manipulate transaction outcomes. By understanding the EVM’s gas stipend mechanism and implementing a secure gas checking mechanism, developers can prevent these attacks and ensure the integrity of their smart contracts.

This article has been written using the following resources: