Table of contents:
- Introduction
- Measuring Gas Consumption
- Run Echidna
- Filtering Out Gas-Reducing Calls
- Summary: Finding transactions with high gas consumption
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
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
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.
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
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.