Woke fuzzer#
Woke fuzzer is a property-based fuzzer for Solidity smart contracts that allows testers to write their fuzz tests in Python.
Installation#
Warning
Woke fuzzer uses eth-brownie package. For this reason, it is recommended to install Woke fuzzer into a virtual environment. It may be also needed to create a brownie-config.yaml configuration file so that Brownie can compile the project.
Woke fuzzer is an optional feature of Woke and can be installed with pip:
Getting started#
To get started, run the following command inside the project directory:
This command creates pytypes
directory, generates smart contract's Python bindings and also copies an example file test_example.py
into tests
directory.
Apart from having useful comments, this example file showcases basic structure of Woke fuzzer's fuzz tests which consists of:
TestingSequence
classes with methods decorated as Flows and Invariants that are used for fuzz testing and an__init__
method which is used forTestingSequence
class setup and can also deploy contracts,test_*
methods that run Woke fuzzer'sCampaign
with theTestingSequence
class.
Tip
All of these can also be separated into multiple files, see a verbose directory structure.
TestingSequence
and Campaign
#
TestingSequence
is a tester defined class that specifies Flows and Invariants that will be later used for fuzz testing and it is also used to set up any prerequisites such as contract deployment or instance attributes later used in tests (contract owner, etc.)
The Campaign
class is a Woke fuzzer class that is responsible for the actual sequence generation and Flows and Invariants execution from TestingSequence
.
The number of generated sequences and Flows must be specified as arguments of the run
function.
The development chain network gets reverted between the sequences.
In the example below, the contract is deployed and the Campaign
is run with 1000 sequences consisting of 400 flows.
import brownie
from pytypes import VotingContractType
from woke.fuzzer import Campaign
from woke.fuzzer.random import random_account
class TestingSequence:
def __init(self, contract: VotingContractType):
self.owner = random_account()
self.contract = contract.deploy({"from": self.owner})
self.subjects = {}
# flows and invariants also go here
def test_campaign(voting_contract: VotingContractType):
for _ in range(1):
brownie.accounts.add()
campaign = Campaign(lambda: TestingSequence(VotingContractType))
campaign.run(1000, 400)
The number of execution times for a Flow per sequence can be tuned using decorators.
Flows#
A Flow is a test method with @flow
decorator that uses the fuzzed smart contract and specifies where Woke fuzzer should insert randomly generated data.
import brownie
from woke.fuzzer.decorators import flow
from woke.fuzzer.random import random_account, random_string
...
@flow
def flow_add_subject(self):
brownie.accounts.add()
subject_name = random_string(0, 10)
subject_account = random_account(
predicate=lambda a: a != self.owner and a not in self.subjects
)
if len(subject_name) == 0 or subject_account in self.subjects:
with brownie.reverts():
self.contract.addSubject(subject_name, {"from": subject_account})
else:
self.contract.addSubject(subject_name, {"from": subject_account})
self.subjects[subject_account] = (subject_name, 0)
A sequence of these Flows is generated and executed by the Campaign
class.
brownie.reverts
is used when a transaction is expected to revert.
Should a contract fail to revert the transaction, it will be reported as a bug by the fuzzer.
Invariants#
Woke fuzzer is a property-based fuzzer, and as such, it allows testers to define Invariant methods with @invariant
decorator. These Invariant methods check for correctness of certain properties in deployed fuzzed smart contracts after every Flow execution.
from woke.fuzzer.decorators import invariant
from woke.fuzzer.random import random_account
...
@invariant
def invariant_subjects(self):
anyone = random_account()
subjects = self.contract.getSubjects({"from": anyone})
assert len(subjects) == len(self.subjects)
for subject in subjects:
anyone = random_account()
subj = self.contract.getSubject(subject, {"from": anyone})
assert self.subjects[subject][0] == subj["name"]
assert self.subjects[subject][1] == subj["votes"]
Should any of the asserts in Invariant method fail, it will be reported as a bug by the fuzzer.
Generating pseudo-random data#
Woke fuzzer has several built-in methods for generating pseudo-random data:
random_account
chooses a random account from existing brownie accounts,random_int
generates random integer but with custom (increased) probabilities formin
,max
and0
,random_bool
picks True/False randomly,random_string
can construct a random string of givenmin
andmax
length,random_bytes
generates a sequence of random bytes with givenmin
andmax
length.
Some of the methods mentioned above also have other optional parameters such as predicates that can be used to further restrict which values will be generated, see source code for full specification.
Running the fuzzer#
After writing Flows and Invariants, Woke fuzzer can be run with all fuzz test files using:
Or with specified fuzz test files:Info
Woke fuzzer runs with multiple processes by default. Be sure to check out woke fuzz --help
for optional CLI arguments.
Checking out the progress#
While fuzzing, the progress is reported in the console stating how many processes are still running.
More verbose logs are stored in .woke-logs/fuzz
directory, specifically the latest
one for the last fuzzing campaign.
Tip
We recommend using less -r
to view the log files because they are ANSI code coloured.
What to do when Woke fuzzer finds a bug#
When Woke fuzzer finds a bug, it will print out a standard Python traceback and ask if a debugger should be attached. With the debugger not being attached, the current process is stopped but the rest of the processes continue fuzzing. The bug can be later checked out in execution logs the same way as when checking out the progress. With the debugger attached, IPython debugger instance is created which allows exploring the state of the fuzzing instance and development chain.
Decorators#
Apart from the @flow
and @invariant
that define type of the test method there are also decorators that can be used to tune Flow selection in a generated sequence:
@weight(x)
- specifies weight that will be used when generating a sequence with default weight being 100. Say flow1 has weight 100 and flow2 has weight 200, flow2 will have ~\(\frac{2}{3}\) of the executions and flow1 only ~\(\frac{1}{3}\),@max_times(x)
- specifies maximum times a Flow will be called in one generated sequence,@ignore
- instructs Woke fuzzer to ignore the decorated Flow or Invariant, useful for testing and debugging.
Optional CLI arguments#
$ woke fuzz --help
Usage: woke fuzz [OPTIONS] [PATHS]...
Run Woke fuzzer.
Options:
-n, --process-count INTEGER Number of processes to create for fuzzing.
-s, --seed TEXT Random seeds
--passive Print one process output into console, run
other in background.
--network TEXT Choose brownie dev chain. Default is
'development' for ganache
--help Show this message and exit.
Fuzzing with multiple processes#
By default, Woke fuzzer performs fuzzing with a number of processes equal to the number of CPU cores. -n
can be used to specify the number of processes.
Development chain#
ganache-cli
is used as a default command to spawn a local dev chain for each process.
It's important to have ports 8545 to (8545 + # of processes) free and bindable.
Woke fuzzer uses dev chain configuration from Brownie so other development chains such as anvil
or hardhat
can be selected with --network
option, but make sure that Brownie actually fully supports the dev chain.
Random seed selection#
To make fuzzing reproducible, it's possible to specify a seed used when generating random data with -s
. With multiple seeds specified, each seed is assigned to a different process and the remaining seeds are generated randomly.
Passive mode#
For debugging purposes, it's possible to run Woke fuzzer in passive mode using the --passive
option. In this mode, Woke fuzzer will print out the output of the process #0
to the console.
Python bindings#
To make working with contracts a bit easier, Woke fuzzer generates Python bindings from smart contract's ABI into the pytypes
directory of the project.
Recommended directory structure#
For bigger projects, we recommend splitting up contract setup, Flow definition and test_*.py
files.