Skip to content

Commit 3a6bf68

Browse files
authored
Merge pull request #40 from crytic/template-modes
Add `template` modes
2 parents 0d3e178 + 18c0fa9 commit 3a6bf68

File tree

6 files changed

+127
-32
lines changed

6 files changed

+127
-32
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ The `template` command is used to generate a fuzzing harness. The harness can in
6969
- `-c`/`--contracts` `target_contracts: list`: The name of the target contract.
7070
- `-o`/`--output-dir` `output_directory: str`: Output directory name. By default it is `fuzzing`
7171
- `--config`: Path to the `fuzz-utils` config JSON file
72+
- `--mode`: The strategy to use when generating the harnesses. Valid options: `simple`, `prank`, `actor`
73+
74+
**Generation modes**
75+
The tool support three harness generation strategies:
76+
- `simple` - The fuzzing harness will be generated with all of the state-changing functions from the target contracts. All function calls are performed directly, with the harness contract as the `msg.sender`.
77+
- `prank` - Similar to `simple` mode, with the difference that function calls are made from different users by using `hevm.prank()`. The users can be defined in the configuration file as `"actors": ["0xb4b3", "0xb0b", ...]`
78+
- `actor` - `Actor` contracts will be generated and all harness function calls will be proxied through these contracts. The `Actor` contracts can be considered as users of the target contracts and the functions included in these actors can be filtered by modifier, external calls, or by `payable`. This allows for granular control over user capabilities.
7279

7380
**Example**
7481

fuzz_utils/parsing/commands/template.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from fuzz_utils.template.HarnessGenerator import HarnessGenerator
77
from fuzz_utils.utils.crytic_print import CryticPrint
88
from fuzz_utils.utils.remappings import find_remappings
9+
from fuzz_utils.utils.error_handler import handle_exit
910

1011

1112
def template_flags(parser: ArgumentParser) -> None:
@@ -26,6 +27,11 @@ def template_flags(parser: ArgumentParser) -> None:
2627
help="Define the output directory where the result will be saved.",
2728
)
2829
parser.add_argument("--config", dest="config", help="Define the location of the config file.")
30+
parser.add_argument(
31+
"--mode",
32+
dest="mode",
33+
help="Define the harness generation strategy you want to use. Valid options are `simple`, `prank`, `actor`",
34+
)
2935

3036

3137
def template_command(args: Namespace) -> None:
@@ -47,6 +53,8 @@ def template_command(args: Namespace) -> None:
4753
config["compilationPath"] = args.compilation_path
4854
if args.name:
4955
config["name"] = args.name
56+
if args.mode:
57+
config["mode"] = args.mode.lower()
5058
config["outputDir"] = output_dir
5159

5260
CryticPrint().print_information("Running Slither...")
@@ -58,3 +66,21 @@ def template_command(args: Namespace) -> None:
5866

5967
generator = HarnessGenerator(config, slither, remappings)
6068
generator.generate_templates()
69+
70+
71+
def check_configuration(config: dict) -> None:
72+
"""Checks the configuration"""
73+
mandatory_configuration_fields = ["mode", "targets", "compilationPath"]
74+
for field in mandatory_configuration_fields:
75+
check_configuration_field_exists_and_non_empty(config, field)
76+
77+
if config["mode"].lower() not in ("simple", "prank", "actor"):
78+
handle_exit(
79+
f"The selected mode {config['mode']} is not a valid harness generation strategy."
80+
)
81+
82+
83+
def check_configuration_field_exists_and_non_empty(config: dict, field: str) -> None:
84+
"""Checks that the configuration dictionary contains a non-empty field"""
85+
if field not in config or len(config[field]) == 0:
86+
handle_exit(f"The template configuration field {field} is not configured.")

fuzz_utils/template/HarnessGenerator.py

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,33 @@ def __init__(
101101
slither: Slither,
102102
remappings: dict,
103103
) -> None:
104-
if "actors" in config:
105-
config["actors"] = check_and_populate_actor_fields(config["actors"], config["targets"])
106-
else:
107-
CryticPrint().print_warning("Using default values for the Actor.")
108-
config["actors"] = self.config["actors"]
109-
config["actors"][0]["targets"] = config["targets"]
104+
self.mode = config["mode"]
105+
match config["mode"]:
106+
case "actor":
107+
if "actors" in config:
108+
config["actors"] = check_and_populate_actor_fields(
109+
config["actors"], config["targets"]
110+
)
111+
else:
112+
CryticPrint().print_warning("Using default values for the Actor.")
113+
config["actors"] = self.config["actors"]
114+
config["actors"][0]["targets"] = config["targets"]
115+
case "simple":
116+
config["actors"] = []
117+
case "prank":
118+
if "actors" in config:
119+
if not isinstance(config["actors"], list[str]) or len(config["actors"]) > 0: # type: ignore[misc]
120+
CryticPrint().print_warning(
121+
"Actors not defined. Using default 0xb4b3 and 0xb0b."
122+
)
123+
config["actors"] = ["0xb4b3", "0xb0b"]
124+
else:
125+
CryticPrint().print_warning(
126+
"Actors not defined. Using default 0xb4b3 and 0xb0b."
127+
)
128+
config["actors"] = ["0xb4b3", "0xb0b"]
129+
case _:
130+
handle_exit(f"Invalid template mode {config['mode']} was provided.")
110131

111132
for key, value in config.items():
112133
if key in self.config and value:
@@ -128,22 +149,33 @@ def generate_templates(self) -> None:
128149
CryticPrint().print_information(
129150
f"Generating the fuzzing Harness for contracts: {self.config['targets']}"
130151
)
131-
132152
# Check if directories exists, if not, create them
133153
check_and_create_dirs(self.output_dir, ["utils", "actors", "harnesses", "attacks"])
134-
# Generate the Actors
135-
actors: list[Actor] = self._generate_actors()
136-
CryticPrint().print_success(" Actors generated!")
154+
137155
# Generate the Attacks
138156
attacks: list[Actor] = self._generate_attacks()
139157
CryticPrint().print_success(" Attacks generated!")
158+
actors: list = []
159+
160+
# Generate actors and harnesses, depending on strategy
161+
match self.mode:
162+
case "actor":
163+
# Generate the Actors
164+
actors = self._generate_actors()
165+
CryticPrint().print_success(" Actors generated!")
166+
case "prank":
167+
actors = self.config["actors"]
168+
case _:
169+
pass
170+
140171
# Generate the harness
141172
self._generate_harness(actors, attacks)
173+
142174
CryticPrint().print_success(" Harness generated!")
143175
CryticPrint().print_success(f"Files saved to {self.config['outputDir']}")
144176

145-
# pylint: disable=too-many-locals
146-
def _generate_harness(self, actors: list[Actor], attacks: list[Actor]) -> None:
177+
# pylint: disable=too-many-locals,too-many-statements,too-many-branches
178+
def _generate_harness(self, actors: list, attacks: list[Actor]) -> None:
147179
CryticPrint().print_information(f"Generating {self.config['name']} Harness")
148180

149181
# Generate inheritance and variables
@@ -155,9 +187,12 @@ def _generate_harness(self, actors: list[Actor], attacks: list[Actor]) -> None:
155187
variables.append(f"{contract.name} {contract.name.lower()};")
156188

157189
# Generate actor variables and imports
158-
for actor in actors:
159-
variables.append(f"Actor{actor.name}[] {actor.name}_actors;")
160-
imports.append(f'import "{actor.path}";')
190+
if self.mode == "actor":
191+
for actor in actors:
192+
variables.append(f"Actor{actor.name}[] {actor.name}_actors;")
193+
imports.append(f'import "{actor.path}";')
194+
elif self.mode == "prank":
195+
variables.append("address[] pranked_actors;")
161196

162197
# Generate attack variables and imports
163198
for attack in attacks:
@@ -176,23 +211,33 @@ def _generate_harness(self, actors: list[Actor], attacks: list[Actor]) -> None:
176211
inputs_str: str = ", ".join(inputs)
177212
constructor += f" {contract.name.lower()} = new {contract.name}({inputs_str});\n"
178213

179-
for actor in actors:
180-
constructor += " for(uint256 i; i < 3; i++) {\n"
181-
constructor_arguments = ""
182-
if actor.contract and hasattr(actor.contract.constructor, "parameters"):
183-
constructor_arguments = ", ".join(
184-
[f"address({x.name.strip('_')})" for x in actor.contract.constructor.parameters]
214+
if self.mode == "actor":
215+
for actor in actors:
216+
constructor += " for(uint256 i; i < 3; i++) {\n"
217+
constructor_arguments = ""
218+
if actor.contract and hasattr(actor.contract.constructor, "parameters"):
219+
constructor_arguments = ", ".join(
220+
[
221+
f"address({x.name.strip('_')})"
222+
for x in actor.contract.constructor.parameters
223+
]
224+
)
225+
constructor += (
226+
f" {actor.name}_actors.push(new Actor{actor.name}({constructor_arguments}));\n"
227+
+ " }\n"
185228
)
186-
constructor += (
187-
f" {actor.name}_actors.push(new Actor{actor.name}({constructor_arguments}));\n"
188-
+ " }\n"
189-
)
229+
elif self.mode == "prank":
230+
for actor in actors:
231+
constructor += f" pranked_actors.push(address({actor}));\n"
190232

191233
for attack in attacks:
192234
constructor_arguments = ""
193235
if attack.contract and hasattr(attack.contract.constructor, "parameters"):
194236
constructor_arguments = ", ".join(
195-
[f"address({x.name.strip('_')})" for x in actor.contract.constructor.parameters]
237+
[
238+
f"address({x.name.strip('_')})"
239+
for x in attack.contract.constructor.parameters
240+
]
196241
)
197242
constructor += f" {attack.name.lower()}Attack = new {attack.name}({constructor_arguments});\n"
198243
constructor += " }\n"
@@ -201,12 +246,26 @@ def _generate_harness(self, actors: list[Actor], attacks: list[Actor]) -> None:
201246

202247
# Generate Functions
203248
functions: list[str] = []
204-
for actor in actors:
205-
function_body = f" {actor.contract.name} selectedActor = {actor.name}_actors[clampBetween(actorIndex, 0, {actor.name}_actors.length - 1)];\n"
206-
temp_list = self._generate_functions(
207-
actor.contract, None, ["uint256 actorIndex"], function_body, "selectedActor"
208-
)
209-
functions.extend(temp_list)
249+
if self.mode == "actor":
250+
for actor in actors:
251+
function_body = f" {actor.contract.name} selectedActor = {actor.name}_actors[clampBetween(actorIndex, 0, {actor.name}_actors.length - 1)];\n"
252+
temp_list = self._generate_functions(
253+
actor.contract, None, ["uint256 actorIndex"], function_body, "selectedActor"
254+
)
255+
functions.extend(temp_list)
256+
else:
257+
for contract in self.targets:
258+
function_body = ""
259+
appended_params = []
260+
if self.mode == "prank":
261+
function_body = " address selectedActor = pranked_actors[clampBetween(actorIndex, 0, pranked_actors.length - 1)];\n"
262+
function_body += " hevm.prank(selectedActor);\n"
263+
appended_params.append("uint256 actorIndex")
264+
265+
temp_list = self._generate_functions(
266+
contract, None, appended_params, function_body, contract.name.lower()
267+
)
268+
functions.extend(temp_list)
210269

211270
for attack in attacks:
212271
temp_list = self._generate_functions(

fuzz_utils/templates/default_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"template": {
1414
"name": "DefaultHarness",
15+
"mode": "simple",
1516
"targets": [],
1617
"outputDir": "./test/fuzzing",
1718
"compilationPath": ".",

fuzz_utils/templates/harness_templates.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
/// --------------------------------------------------------------------
3030
3131
import "{{remappings["properties"]}}util/PropertiesHelper.sol";
32+
import "{{remappings["properties"]}}util/Hevm.sol";
3233
{% for import in target.imports -%}
3334
{{import}}
3435
{% endfor %}

tests/test_harness.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data"
1414
default_config = {
1515
"name": "DefaultHarness",
16+
"mode": "actor",
1617
"compilationPath": ".",
1718
"targets": [],
1819
"outputDir": "./test/fuzzing",

0 commit comments

Comments
 (0)