• YouTube Channel
  • System Status
  • VS Code Extension
  • Call Without Gas Budget

    Overview

    What is the Call Without Gas Budget vulnerability?

    Solidity supports various methods of making cross-smart-contract calls, where a function on one smart contract is invoked by another rather than directly by the user of an externally-owned account. When making a cross-contract call, it is possible to specify the amount of gas (used to "pay" for computation) to forward to the called contract. If this gas amount is not specified, an external function may either maliciously or inadvertently consume all remaining gas budgeted for the whole transaction, which could cause it to fail.

    It is important to note that there is a tradeoff to be made here: the Solidity documentation discourages setting explicit gas amounts in external calls for forward compatibility reasons, citing possible changes to opcode costs in the future. While these opcode repricings have been uncommon, they have happened in the past, so it may be judicious to strike a middle ground when specifying gas budgets, and not set them strictly based on the current cost of the external call.

    Further reading: Solidity Documentation: External Function Calls

    Technical example of vulnerable code

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract FlawedOrMaliciousExternalContract {
          // Function that will consume all gas allotted to it
          function flawedExternalFunction(uint256 value) external returns (uint256) {
              uint256 counter = 0;
    
              while(counter < 10) {
                  value += 1;
              }
    
              return value;
          }
      }
        
      contract ConsumerContract {
          FlawedOrMaliciousExternalContract c;
    
          constructor(address externalContractAddress) {
              c = FlawedOrMaliciousExternalContract(externalContractAddress);
          }
    
          // This function makes an external call expecting to do something after receiving the return value
          function useExternalFunctionality(address _target) external {
              (bool success, uint256 calculatedValue) = _target.call(
                  abi.encodeWithSignature("flawedExternalFunction(uint256)", 10)
              );
    
              if (success) {
    
                  // do something with calculatedValue
    
              } else {
    
                  // attempt to do something else
    
              }
          }
      }
    
    

    In the above example, whether by malice or oversight, flawedExternalFunction() in FlawedOrMaliciousExternalContract will consume all gas forwarded to it. In contract ConsumerContract, the function useExternalFunctionality() does not specify a gas budget while using the low-level address.call() operation, and will thus forward the maximal amount of remaining gas. Because this function has additional code following the external call, that conditional code may never execute.

    Technical example of how to fix the vulnerability

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract FlawedOrMaliciousExternalContract {
          // Function that will consume all gas allotted to it
          function flawedExternalFunction(uint256 value) external returns (uint256) {
              uint256 counter = 0;
    
              while(counter < 10) {
                  value += 1;
              }
    
              return value;
          }
      }
    
      contract ConsumerContract {
          FlawedOrMaliciousExternalContract c;
    
          constructor(address externalContractAddress) {
              c = FlawedOrMaliciousExternalContract(externalContractAddress);
          }
    
          // This function makes an external call expecting to do something after receiving the return value
          function useExternalFunctionality(address _target) external {
              (bool success, uint256 calculatedValue) = _target.call{gas: 10000}(
                  abi.encodeWithSignature("flawedExternalFunction(uint256)", 10)
              );
    
              if (success) {
    
                  // do something with calculatedValue
    
              } else {
    
                  // attempt to do something else
    
              }
          }
      }
    
    

    In the revised example above, the external call now uses the named argument {gas: 10000} to forward a maximum of 10,000 gas to the external contract. If an invocation of useExternalFunctionality() accounts for this as well as the subsequent conditional code, the rest of the function can finish execution even if the external logic is flawed.