Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: high
Invalid

Unrestricted Gas Forwarding in External Calls, having limitless gas is unsafe

Summary: callExternalContract makes an external call to another contract, but there is no limit on the amount of gas sent. This can lead to unexpected results. We need to have a mechanism to set a limit on gas.

/// @notice Performs an external call to another contract
/// @param contractAddress The address of the external contract
/// @param data The calldata to be sent
/// @return result The bytes result of the external call
function callExternalContract(address contractAddress, bytes memory data) external payable onlyRole(EXTERNAL_CALLER) returns (bytes memory ) {
(bool success, bytes memory returndata) = contractAddress.call{value: msg.value}(data);
require(success, "External call failed");
return returndata;
}

Vulnerability Details: It can lead to unexpected behaviour. It might be the case that the contract is designed in a way to allow flexibility for varying gas needs. But, in extreme cases, it can lead to catastrophic behaviour. Why not have a gas limit?

With no gas limit, the chances are:

Increased Gas Costs: Without a limit, a call may consume unexpectedly high gas, which could deplete the caller’s funds or affect the gas efficiency of the overall system.

Denial of Service (DoS): If excessive gas consumption exhausts the sender’s balance or exceeds the gas stipend, subsequent operations or other contract interactions may fail, causing potential DoS issues within the contract's flow.

There are other possibilities, however low, but still there.

The lack of gas control can become a high vulnerability, particularly when combined with reentrancy risks or untrusted external calls:

  1. Reentrancy Attacks: If the external call is to a contract that might reenter and manipulate the calling contract’s state, setting a gas limit is critical. Unrestricted gas allows malicious contracts to perform multiple actions, which can exploit reentrancy vulnerabilities and potentially drain funds or manipulate contract state.

  2. Untrusted Contract Interactions: When the external call involves interactions with unknown or potentially malicious contracts, unrestricted gas opens up the possibility for complex and unpredictable contract behaviors. An untrusted contract could craft a complex call sequence to drain resources or interfere with contract logic.

  3. Unexpected Failures: Lack of a gas limit can cause unexpected behavior in cases where gas usage surges unpredictably, which may interfere with critical functions and data consistency, especially in complex multi-call flows.

Impact: Let's look into some scenarios here. Some might not apply but still, we need to be aware of.

1. Reentrancy Vulnerability

  • Description: When calling an external contract without specifying a gas limit, all available gas is forwarded to the external call by default. This can enable malicious contracts to perform complex reentrant calls that take advantage of the unbounded gas.

  • Exploitation:

    • If the external contract (contractAddress) reenters and makes a recursive call back to the original function, it may lead to repeated state changes or withdrawals, especially if state updates are performed after the external call.

    • In a common reentrancy attack, the malicious contract repeatedly reenters the external call, exploiting the unrestricted gas to drain the contract’s funds or manipulate state variables.

  • Impact: Potential loss of funds, incorrect state updates, or data corruption.

  • Example Scenario:

    // Vulnerable code with no reentrancy guard and unlimited gas forwarding
    function withdraw() external {
    uint256 balance = balances[msg.sender];
    require(balance > 0, "Insufficient balance");
    // Unrestricted external call without gas limit and no reentrancy protection
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success, "Transfer failed");
    // Update balance after external call
    balances[msg.sender] = 0;
    }

    In this example, the attacker can use reentrancy to repeatedly call withdraw() before the balances[msg.sender] = 0; update, draining funds.

2. Denial of Service (DoS) Attack via Gas Exhaustion

  • Description: When no gas limit is set, an external contract could consume excessive gas, potentially exhausting the caller’s available gas and causing an unexpected failure. This can lead to DoS conditions, preventing the calling function or even other functions from completing successfully.

  • Exploitation:

    • Malicious contracts or unoptimized contracts could intentionally include gas-heavy operations, such as recursive loops or highly complex computations.

    • Since the caller’s gas can be depleted, other users attempting to call the function or related functions may encounter failures, effectively creating a DoS scenario.

  • Impact: Disruption of contract functionality, transaction failures, or inability to perform critical operations.

  • Example Scenario:

    function processRequest(address externalContract, bytes memory data) external {
    // Call external contract without gas limit
    (bool success, ) = externalContract.call(data);
    require(success, "External call failed");
    }

    If externalContract executes gas-heavy operations, the call might exhaust the gas provided by the caller, causing processRequest() to fail repeatedly.

3. Gas Griefing Attack

  • Description: Gas griefing attacks exploit unlimited gas forwarding to cause unwanted high gas usage for the user or contract owner. When gas is unbounded, even a non-malicious external contract can sometimes consume more gas than expected, resulting in expensive transactions for the caller.

  • Exploitation:

    • An attacker could deploy a contract with deliberately inefficient functions, increasing gas costs when interacting with the original contract.

    • If users or other contracts are unable to estimate gas limits properly due to the unpredictably high gas use, they may face unnecessary expenses or fail to execute transactions.

  • Impact: Unintended high gas fees, potentially limiting user participation or driving up operating costs.

  • Example Scenario:

    function performOperation(address targetContract, bytes memory payload) external payable {
    // No gas limit set, allowing external call to potentially use excessive gas
    (bool success, ) = targetContract.call{value: msg.value}(payload);
    require(success, "External call failed");
    }

    Here, if targetContract has a gas-heavy function, performOperation() could become cost-prohibitive for the caller due to high gas usage.

4. Malicious Gas Consumption with Fallback Functions

  • Description: When calling untrusted contracts without a gas limit, a malicious contract’s fallback function could consume an excessive amount of gas each time it’s called, either in a loop or through multiple fallback triggers.

  • Exploitation:

    • A fallback function in an attacker’s contract could drain gas through repetitive invocations or deliberately inefficient code, forcing the original function to fail or deplete resources.

    • Since fallback functions can be designed to handle arbitrary calls, they’re particularly risky when allowed unbounded gas, especially in interactions that require repeated or recursive calls.

  • Impact: Contract operation failures, resource depletion, increased gas fees, or user dissatisfaction.

  • Example Scenario:

    function executeTransaction(address target, bytes memory data) external {
    // Unrestricted call that could trigger fallback functions
    (bool success, ) = target.call(data);
    require(success, "Transaction failed");
    }

    An attacker’s contract could have a fallback function that consumes gas heavily, causing executeTransaction to fail or incur high costs repeatedly.

Proof of Concept Code: Here is a proof of concept (PoC) demonstrating the excessive gas usage vulnerability when calling an external contract without a gas limit. This PoC consists of two parts:

  1. Vulnerable Contract: The contract that calls an external contract without specifying a gas limit, allowing excessive gas consumption.

  2. Gas Griefing Contract: A contract designed to exploit the vulnerable contract by executing a gas-intensive operation when called.

1. Vulnerable Contract

This contract has a function callExternal() that calls an external contract without setting a gas limit. This lack of gas restriction allows a malicious or poorly optimized contract to consume an excessive amount of gas, potentially causing the transaction to fail or incurring unexpectedly high gas costs.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableContract {
event CallSuccess(bool success, bytes data);
// Function to call an external contract without a gas limit
function callExternal(address target, bytes memory data) external {
// External call without a gas limit, susceptible to gas griefing
(bool success, bytes memory returnData) = target.call(data);
require(success, "External call failed");
emit CallSuccess(success, returnData);
}
}

2. Gas Griefing Contract

This contract performs a gas-intensive operation (such as a loop) when it is called. It’s used to demonstrate the excessive gas usage vulnerability in VulnerableContract’s callExternal() function.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GasGriefingContract {
event LoopExecuted(uint256 iterations);
// A function that performs a gas-intensive operation
function consumeGas() external {
uint256 iterations = 1000;
// Loop to consume gas
for (uint256 i = 0; i < iterations; i++) {
// Arbitrary operation to consume gas
if (i % 2 == 0) {
i + 1;
} else {
i - 1;
}
}
emit LoopExecuted(iterations);
}
}

How the Exploit Works

  1. Setup: Deploy VulnerableContract and GasGriefingContract.

  2. Attack:

    • The attacker calls VulnerableContract.callExternal() with GasGriefingContract as the target and the consumeGas() function as data.

    • VulnerableContract forwards an unbounded amount of gas to GasGriefingContract, which performs a gas-intensive loop in consumeGas().

    • Since VulnerableContract does not limit the gas, GasGriefingContract can consume an excessive amount of gas, either causing the transaction to fail due to out-of-gas errors or forcing the user to pay unnecessarily high gas fees.

Example of Calling the Exploit

Here’s how to perform the exploit by calling callExternal():

// Assume `vulnerable` is an instance of VulnerableContract
// Assume `griefing` is an instance of GasGriefingContract
// Encode the function call to consumeGas() in GasGriefingContract
bytes memory data = abi.encodeWithSignature("consumeGas()");
// Call the vulnerable contract, which will forward unbounded gas to GasGriefingContract
vulnerable.callExternal(address(griefing), data);

Expected Results

  1. High Gas Fees: The call to consumeGas() in GasGriefingContract will consume a lot of gas, potentially causing high transaction fees for the caller.

  2. Transaction Failure: If the gas consumption exceeds the caller’s gas limit, the transaction will fail with an out-of-gas error.

Mitigation

To prevent this vulnerability, the VulnerableContract should set a reasonable gas limit for external calls:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SecureContract {
event CallSuccess(bool success, bytes data);
function callExternal(address target, bytes memory data) external {
uint256 gasLimit = 50000; // Set a sensible gas limit
// Call the external contract with a gas limit
(bool success, bytes memory returnData) = target.call{gas: gasLimit}(data);
require(success, "External call failed");
emit CallSuccess(success, returnData);
}
}

Summary

This PoC demonstrates how a lack of gas limit control in external calls can lead to gas griefing attacks, where a malicious contract can cause the caller to pay high fees or experience transaction failures. By setting a gas limit, you can prevent unbounded gas consumption, making the contract safer and more predictable in terms of gas usage.

4o

Tools Used: VS code

Recommendations: Set Gas Limits on External Calls: Forward a set amount of gas to the external call to cap its resource usage. For instance, target.call{gas: 50000}(data) limits the call to 50,000 gas units.

  • Use Reentrancy Guards: Always apply nonReentrant where external calls are made to prevent recursive exploitation.

  • Perform State Changes Before External Calls: Critical state updates (e.g., balances, access rights) should be completed before the external call to ensure that a reentrant call cannot manipulate the contract’s state unexpectedly.

  • Avoid Direct Calls to Untrusted Contracts: If possible, minimize direct interaction with contracts that are not verified or known to be trusted. Use proxy contracts or intermediaries to interact with unknown contracts.

  • Add Comprehensive Logging: Include events for successful and failed calls to monitor suspicious activities, which can help detect early signs of gas-based abuse or unusual external interactions.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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