Identifying High Gas Consumption Transactions

Table of contents:

Introduction

This guide demonstrates how to identify transactions with high gas consumption using Echidna. The target is the following smart contract (gas.sol):

contract C {
    uint256 state;

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

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

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

The expensive function can have significant gas consumption.

Currently, Echidna always requires a property to test - in this case, echidna_test always returns true. We can run Echidna to verify this:

echidna gas.sol
...
echidna_test: passed! 🎉

Seed: 2320549945714142710

Measuring Gas Consumption

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

estimateGas: true

In this example, we'll also reduce the size of the transaction sequence for easier interpretation:

seqLen: 2
estimateGas: true

Running Echidna

With the configuration file created, we can run Echidna as follows:

echidna 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 displayed gas is an estimation provided by HEVM.

Excluding Gas-Reducing Calls

The tutorial on filtering functions to call during a fuzzing campaign demonstrates how to remove certain functions during testing. This can be crucial for obtaining accurate gas estimates. 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(0);
    }

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

With this config.yaml, Echidna can call all functions but won't easily identify transactions with high gas consumption:

echidna 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

This occurs because the cost depends on the size of addrs, and random calls tend to leave the array almost empty. By blacklisting pop and clear, we obtain better results (blacklistpushpop.yaml):

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

Summary: Identifying high gas consumption transactions

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

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

After completing the fuzzing campaign, Echidna will report a sequence with the maximum gas consumption for each function.