Skip to content

Latest commit

 

History

History
160 lines (124 loc) · 3.96 KB

finding-transactions-with-high-gas-consumption.md

File metadata and controls

160 lines (124 loc) · 3.96 KB

Finding transactions with high gas consumption

Table of contents:

Introduction

We will see how to find the transactions with high gas consumption with Echidna. The target is the following smart contract (example/gas.sol):

contract C {
  uint state;

  function expensive(uint8 times) internal {
    for(uint8 i=0; i < times; i++)
      state = state + i;
  }

  function f(uint x, uint y, uint8 times) public {
    if (x == 42 && y == 123)
      expensive(times);
    else
      state = 0;
  }

  function echidna_test() public returns (bool) {
    return true;
  }

}

Here expensive can have a large gas consumption.

Currently, Echidna always needs a property to test: here echidna_test always returns true. We can run Echidna to verify this:

$ echidna-test gas.sol
...
echidna_test: passed! 🎉

Seed: 2320549945714142710

Measuring Gas Consumption

To enable Echidna's gas consumption feature, create a configuration file config.yaml:

estimateGas: true

In this example, we will also reduce the size of the transaction sequence to make the results easier to understand:

seqLen: 2
estimateGas: true

Run Echidna

Once we have the configuration file created, we can run Echidna like this:

$ echidna-test gas.sol --config config.yaml 
...
echidna_test: passed! 🎉

f used a maximum of 1333608 gas
  Call sequence:
    f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2

Unique instructions: 157
Unique codehashes: 1
Seed: -325611019680165325

  • The gas shown is an estimation provided by HEVM.

Filtering Out Gas-Reducing Calls

The tutorial on filtering functions to call during a fuzzing campaign shows how to remove some functions during testing.
This can be critical for getting an accurate gas estimate. Consider the following example (example/pushpop.sol):

contract C {
  address [] addrs;
  function push(address a) public {
    addrs.push(a);
  }
  function pop() public {
    addrs.pop();
  }
  function clear() public{
    addrs.length = 0;
  }
  function check() public{
    for(uint256 i = 0; i < addrs.length; i++)
      for(uint256 j = i+1; j < addrs.length; j++)
        if (addrs[i] == addrs[j])
          addrs[j] = address(0x0);
  }
  function echidna_test() public returns (bool) {
      return true;
  }
}

If Echidna uses this config.yaml, it can call all functions and won't easily find transactions with high gas cost:

$ echidna-test pushpop.sol --config config.yaml
...
pop used a maximum of 10746 gas
...
check used a maximum of 23730 gas
...
clear used a maximum of 35916 gas
...
push used a maximum of 40839 gas

That's because the cost depends on the size of addrs and random calls tend to leave the array almost empty. Blacklisting pop and clear, however, gives us much better results (example/blacklistpushpop.yaml):

estimateGas: true
filterBlacklist: true
filterFunctions: ["C.pop()", "C.clear()"]
$ echidna-test pushpop.sol --config config.yaml
...
push used a maximum of 40839 gas
...
check used a maximum of 1484472 gas

Summary: Finding transactions with high gas consumption

Echidna can find transactions with high gas consumption using the estimateGas configuration option:

estimateGas: true
$ echidna-test contract.sol --config config.yaml 
...

Once the fuzzing campaign is over, Echidna will report a sequence with the maximum gas consumption for every function.