Solidity – How to Get the Gas Cost of Non-Constant Function Calls

gassoliditystate-variable

In my understanding, there are some situations, where the compiler can't calculate, how much Gas it takes to complete the function call. The most obvious example – when your function is calling some function from the another contract. The compiler doesn't have the source of the another contract so it can't tell you, how much Gas is necessary, to complete the call.

But even when the function doesn't call any external function, the compiler also can't tell you the cost. A small example is listed bellow:

pragma solidity ^0.4.16;

contract Test {
    address[] owners;

    function addOwner(address newOwner) external {
        owners.push(newOwner);
    }
}

The warning message is:

Gas requirement of function browser/ballot.sol:Test.addOwner(address)
unknown or not constant. If the gas requirement of a function is
higher than the block gas limit, it cannot be executed. Please avoid
loops in your functions or actions that modify large areas of storage
(this includes clearing or copying arrays in storage)

What is the reason of this warning? I think maybe it's because of changing the state, but I'm not sure.

Thank in advance!

Best Answer

You can get the gas estimations by adding --gas to solc:

> solc --gas Test.sol

======= Test.sol:Test =======
Gas estimation:
construction:
   118 + 70800 = 70918
external:
   addOwner(address):   infinite

However, there are many cases when the gas estimator reports infinite gas. It doesn't necessarily mean that there is an infinite loop in your code or that your code is incorrect but just the estimator is quite restrictive when making decisions about how much gas can be consumed by the code. In particular, any backward jumps or loops in the assembly code will make it report infinite gas.


Detailed answer below

The warning comes from the Remix' static code analyser. From remix sources

  if (gas === null || gas >= 3000000) {
    report.push({
      warning: yo`<span>Gas requirement of function ${contractName}.${functionName} ${gasString}.<br />
      If the gas requirement of a function is higher than the block gas limit, it cannot be executed.
      Please avoid loops in your functions or actions that modify large areas of storage
      (this includes clearing or copying arrays in storage)</span>`
    })
  }

Debugging shows that the gas is null.

Gas estimations come from the solidity compiler. In this particular case the compiler estimated the gas for the addOwner function as infinity. You can see it by running solc compiler with --gas option:

> solc --gas Test.sol

======= Test.sol:Test =======
Gas estimation:
construction:
   118 + 70800 = 70918
external:
   addOwner(address):   infinite

From solidity compiler sources you can see that functional estimation is done in the GasEstimator.cpp which in turn uses PathGasMeter.cpp to estimate the maximum possible gas consumption (I marked with arrow the place where infinite gas estimation is returned).

if (item.type() == Tag || item == AssemblyItem(Instruction::JUMPDEST))
{
    // Do not allow any backwards jump. This is quite restrictive but should work for
    // the simplest things.
    if (path->visitedJumpdests.count(index))
        return GasMeter::GasConsumption::infinite(); // <---------------
    path->visitedJumpdests.insert(index);
}

// Do not allow any backwards jump. This is quite restrictive but should work for the simplest things.

Backward jumps indicate a loop which might result in unbounded gas consumption.

The piece of assembly output from solc --asm Test.sol that has a backward jump is:

tag_14:
      dup1
      dup3
      gt
      iszero
      tag_15
      jumpi
      0x0
      dup2
      0x0
      swap1
      sstore
      pop
      0x1
      add
      jump(tag_14)

This piece of opcodes clears the storage in case the length of an array is reduced. Since the storage is expensive, every time it's cleared the gas is refunded to the transaction sender.

Why would that contract need to reduce the length of the array you might ask. The reason is

arr.push[element];

is replaced by

arr.length = arr.length + 1;
arr[arr.length] = element;

The first line where the length of the array is modified is further expanded by the function that's changing the array length which in case the length is lesser than it was before will clear the storage slots not used by the array. This function iterates through unused slots and clears them one by one.

Although our contract never needs to reduce the array size the assembler includes this piece of code nevertheless which causes the gas estimator to report the infinite max gas usage.

You can try executing the addOwner(address) function multiple times. The used gas is always the same: 48829 gas. However if you add another function to the contract:

function setOwnersLength(uint newLength) public {
    owners.length = newLength;
}

and try calling it you will see that the used gas depends on by how much you reduce the array length.

Related question: Infinite gas estimation from solc for simple function

Related Topic