Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions eth_vertigo/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from pathlib import Path
from eth_vertigo.core import MutationResult
from eth_vertigo.core.network import DynamicNetworkPool, StaticNetworkPool, Ganache
from eth_vertigo.interfaces import foundry
from eth_vertigo.interfaces.foundry import FoundryCampaign
from eth_vertigo.interfaces.truffle import TruffleCampaign
from eth_vertigo.interfaces.hardhat import HardhatCampaign
from eth_vertigo.core.filters.sample_filter import SampleFilter
Expand Down Expand Up @@ -135,6 +137,16 @@ def run(
filters=filters,
suggesters=test_suggesters,
)

if project_type == "foundry":
campaign = FoundryCampaign(
foundry_command=["forge"],
project_directory=project_path,
mutators=mutators,
network_pool=network_pool,
filters=filters,
suggesters=test_suggesters,
)
except:
click.echo("[-] Encountered an error while setting up the core campaign")
if isinstance(network_pool, DynamicNetworkPool):
Expand Down Expand Up @@ -205,8 +217,12 @@ def _directory_type(working_directory: str):
wd = Path(working_directory)
has_truffle_config = (wd / "truffle.js").exists() or (wd / "truffle-config.js").exists()
has_hardhat_config = (wd / "hardhat.config.js").exists()
has_foundry_config = (wd / "foundry.toml").exists()

if has_truffle_config and not has_hardhat_config:
return "truffle"
if has_hardhat_config and not has_truffle_config:
return "hardhat"
if has_foundry_config:
return "foundry"
return None
24 changes: 22 additions & 2 deletions eth_vertigo/interfaces/common/tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@

from typing import List, Dict, Union

def normalize_foundry(result) -> Dict[str, TestResult]:
tests = {}
if 'Failing tests' in result:
results = loads(result[:result.index('Failing tests')])
else:
results = loads(result)

for _tests in results:
filename = _tests.split(":")[1]
for test in results[_tests]["test_results"]:
name = test.split("(")[0]
result = results[_tests]["test_results"][test]
if result["success"]:
tests[f"{filename} {name}"] = TestResult(name, f"{filename} {name}", 0, True)
else:
tests[f"{filename} {name}"] = TestResult(name, f"{filename} {name}", 0, False)

return tests

def normalize_mocha(mocha_json: dict) -> Dict[str, TestResult]:
tests = {}
Expand Down Expand Up @@ -131,10 +149,12 @@ def run_test_command(
test_result.append(line)

test_result = "\n".join(test_result)

if errors:
raise TestRunException("\n".join(errors))
try:
return normalize_mocha(loads(test_result))
if "failures" in test_result:
return normalize_mocha(loads(test_result))
else:
return normalize_foundry(test_result)
except JSONDecodeError:
raise TestRunException("Encountered error during test output analysis")
65 changes: 65 additions & 0 deletions eth_vertigo/interfaces/foundry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from eth_vertigo.core.campaign import BaseCampaign
from typing import List
from eth_vertigo.mutator.mutator import Mutator
from pathlib import Path
from eth_vertigo.core.network import NetworkPool
from json import loads


class FoundryCampaign(BaseCampaign):
def __init__(
self,
foundry_command: List[str],
project_directory: Path,
mutators: List[Mutator],
network_pool: NetworkPool,
filters=None,
suggesters=None
):
from eth_vertigo.interfaces.foundry.tester import FoundryTester
from eth_vertigo.interfaces.foundry.compile import FoundryCompiler
from eth_vertigo.interfaces.foundry.mutator import FoundrySourceFile

compiler = FoundryCompiler(foundry_command)
tester = FoundryTester(foundry_command, str(project_directory), compiler)
source_file_builder = lambda ast, full_path: FoundrySourceFile(ast, full_path)

super().__init__(
project_directory=project_directory,
mutators=mutators,
network_pool=network_pool,

compiler=compiler,
tester=tester,
source_file_builder=source_file_builder,

filters=filters,
suggesters=suggesters
)

def _get_sources(self):
""" Implements basic mutator file discovery """
contracts_dir = self.project_directory / "out"
if not contracts_dir.exists():
self.compiler.run_compilation(str(self.project_directory))

cache_dir = self.project_directory / "cache"
if not cache_dir.exists():
self.compiler.run_compilation(str(self.project_directory))

cache = loads(Path(f"{cache_dir}/solidity-files-cache.json").read_text("utf-8"))
files = cache["files"]

for file in files:
if file.startswith("src/"):
contract = files[file]
basename = Path(contract["sourceName"]).name
stem = Path(contract["sourceName"]).stem

build_info_file = contracts_dir/basename/(stem + ".json")
build_info = loads(build_info_file.read_text("utf-8"))

ast = build_info["ast"]
absolute_path = self.project_directory / ast["absolutePath"]

yield self.source_file_builder(ast, absolute_path)
65 changes: 65 additions & 0 deletions eth_vertigo/interfaces/foundry/compile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from eth_vertigo.interfaces.foundry.core import FoundryCore
from eth_vertigo.interfaces.common import strip_metadata
from eth_vertigo.interfaces.generics import Compiler
from typing import Dict

from subprocess import Popen, TimeoutExpired
from tempfile import TemporaryFile
from pathlib import Path

from loguru import logger
import json

class FoundryCompiler(Compiler, FoundryCore):
def run_compilation(self, working_directory: str) -> None:
with TemporaryFile() as stdin, TemporaryFile() as stdout, TemporaryFile() as stderr:
stdin.seek(0)
proc = Popen(
self.foundry_command + ['build'],
stdin=stdin,
stdout=stdout,
stderr=stderr,
cwd=working_directory
)
proc.wait()
stdout.seek(0)
output = stdout.read()

split = output.decode('utf-8').split("\n")

errors = []
for line in split:
if line.startswith("Error"):
errors.append(line)

if errors:
raise Exception("Encountered compilation error: \n" + "\n".join(errors))

def get_bytecodes(self, working_directory: str) -> Dict[str, str]:
""" Returns the bytecodes in the compilation result of the current directory

:param working_directory: The truffle directory for which we retreive the bytecodes
:return: bytecodes in the shape {'contractName': '0x00'}
"""
w_dir = Path(working_directory)
self.run_compilation(working_directory)
contracts_dir = w_dir / "out"

if not (contracts_dir).is_dir():
logger.error("Compilation did not create out directory")

current_bytecode = {}

for contract in contracts_dir.iterdir():
if not contract.name.endswith(".t.sol") and not contract.name.endswith(".s.sol"):
try:
contract = contract/ (contract.stem+".json")
contract_compilation_result = json.loads(contract.read_text('utf-8'))
except json.JSONDecodeError:
logger.warning(f"Could not read compilation result for {contract.name}")
continue

current_bytecode[contract.stem] = strip_metadata(contract_compilation_result["bytecode"]["object"])

return current_bytecode

3 changes: 3 additions & 0 deletions eth_vertigo/interfaces/foundry/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FoundryCore:
def __init__(self, foundry_command):
self.foundry_command = foundry_command
113 changes: 113 additions & 0 deletions eth_vertigo/interfaces/foundry/mutator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from jsonpath_rw import parse
from json import loads
from pathlib import Path
from eth_vertigo.mutator.source_file import SourceFile
from typing import Dict

def _get_ast(json_file):
return loads(json_file.read_text("utf-8"))


def _get_src(src_str: str):
return [int(e) for e in src_str.split(":")]


def _get_binaryop_info(node: dict):
"""
Gets info on the binary operation from an ast node
This ast node must be referencing an binary operation

:param node: ast node to look for
:return: the operator, src for the operator
"""
if node["nodeType"] != "BinaryOperation":
raise ValueError("Passed node is not a binary operation")

c_src = _get_src(node["src"])

original_operator = node["operator"]
op0_src = _get_src(node["leftExpression"]["src"])
op1_src = _get_src(node["rightExpression"]["src"])

if not (c_src[2] == op0_src[2] == op1_src[2]):
raise ValueError("src fields are inconsistent")

start = op0_src[0] + op0_src[1]
length = op1_src[0] - start
op_src = (start, length, c_src[2])

return original_operator, op_src


def _get_op_info(node: dict):
c_src = _get_src(node["src"])

original_operator = node["operator"]
op0_src = _get_src(node["leftHandSide"]["src"])
op1_src = _get_src(node["rightHandSide"]["src"])

if not (c_src[2] == op0_src[2] == op1_src[2]):
raise ValueError("src fields are inconsistent")

start = op0_src[0] + op0_src[1]
length = op1_src[0] - start
op_src = (start, length, c_src[2])

return original_operator, op_src


class FoundrySourceFile(SourceFile):
def __init__(self, ast: Dict, file: Path):
self.ast = ast
super().__init__(file)

def get_binary_op_locations(self):
path_expr = parse('*..nodeType.`parent`')
for match in path_expr.find(self.ast):
if match.value["nodeType"] != "BinaryOperation":
continue
yield _get_binaryop_info(match.value)

def get_if_statement_binary_ops(self):
path_expr = parse('*..nodeType.`parent`')
for match in path_expr.find(self.ast):
if match.value["nodeType"] != "IfStatement":
continue
condition = match.value["children"][0]
yield _get_binaryop_info(condition)

def get_assignments(self):
path_expr = parse('*..nodeType.`parent`')
for match in path_expr.find(self.ast):
if match.value["nodeType"] != "Assignment":
continue
yield _get_op_info(match.value)

def get_void_calls(self):
path_expr = parse('*..nodeType.`parent`')
for match in path_expr.find(self.ast):
if match.value["nodeType"] != "FunctionCall":
continue
function_identifier = match.value["expression"]

function_typedef = function_identifier["typeDescriptions"]["typeString"]
if "returns" in function_typedef:
continue
if "function" not in function_typedef:
continue
if function_identifier["typeDescriptions"]["typeIdentifier"].startswith("t_function_event"):
continue

try:
if "require" in function_identifier["name"]:
continue
except KeyError:
continue
yield (None, _get_src(match.value["src"]))

def get_modifier_invocations(self):
path_expr = parse('*..nodeType.`parent`')
for match in path_expr.find(self.ast):
if match.value["nodeType"] != "ModifierInvocation":
continue
yield (None, _get_src(match.value["src"]))
33 changes: 33 additions & 0 deletions eth_vertigo/interfaces/foundry/tester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pathlib import Path
from typing import List, Optional

from eth_vertigo.interfaces.common.tester import MochaStdoutTester
from eth_vertigo.interfaces.generics import Compiler
from eth_vertigo.interfaces.foundry.core import FoundryCore


def _set_reporter(directory: str):
pass


def _set_include_tests(directory: str, test_names: List[str]):
pass


class FoundryTester(FoundryCore, MochaStdoutTester):
def __init__(self, foundry_location, project_directory, compiler: Compiler):
self.project_directory = project_directory
self.compiler = compiler
self.foundry_location = foundry_location
FoundryCore.__init__(self, foundry_location)

def instrument_configuration(self, working_directory, keep_test_names: Optional[List[str]]):
_set_reporter(working_directory)
if keep_test_names:
_set_include_tests(working_directory, keep_test_names)

def build_test_command(self, network: Optional[str]) -> List[str]:
result = self.foundry_location + ['test','-j']
if network:
result.extend(['--fork-url', network])
return result