diff --git a/.github/scripts/tool_test_runner.sh b/.github/scripts/tool_test_runner.sh index 30d8176a0..574358e10 100755 --- a/.github/scripts/tool_test_runner.sh +++ b/.github/scripts/tool_test_runner.sh @@ -2,11 +2,11 @@ # used to pass --cov=$path and --cov-append to pytest if [ "$1" != "" ]; then - pytest "$1" tests/tools/read-storage/test_read_storage.py + pytest "$1" tests/tools status_code=$? python -m coverage report else - pytest tests/tools/read-storage/test_read_storage.py + pytest tests/tools status_code=$? fi diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 29f0ac2e0..b039f69d9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -47,7 +47,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Docker Build and Push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 target: final diff --git a/.github/workflows/pip-audit.yml b/.github/workflows/pip-audit.yml index 1c0a1d40a..f4a028c8c 100644 --- a/.github/workflows/pip-audit.yml +++ b/.github/workflows/pip-audit.yml @@ -34,6 +34,6 @@ jobs: python -m pip install . - name: Run pip-audit - uses: pypa/gh-action-pip-audit@v1.0.8 + uses: pypa/gh-action-pip-audit@v1.1.0 with: virtual-environment: /tmp/pip-audit-env diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 031d1a376..0a0f04f2b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,10 +44,10 @@ jobs: path: dist/ - name: publish - uses: pypa/gh-action-pypi-publish@v1.8.14 + uses: pypa/gh-action-pypi-publish@v1.9.0 - name: sign - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: ./dist/*.tar.gz ./dist/*.whl release-signing-artifacts: true diff --git a/Dockerfile b/Dockerfile index 6de5ec2c6..c65efc22e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM ubuntu:jammy AS python-wheels RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ gcc \ git \ + make \ python3-dev \ python3-pip \ && rm -rf /var/lib/apt/lists/* diff --git a/scripts/ci_test_upgradability.sh b/scripts/ci_test_upgradability.sh index 0a0d77f51..a4da93873 100755 --- a/scripts/ci_test_upgradability.sh +++ b/scripts/ci_test_upgradability.sh @@ -2,7 +2,8 @@ ### Test slither-check-upgradeability -DIR_TESTS="tests/check-upgradeability" +DIR_TESTS="tests/tools/check_upgradeability" +solc-select install "0.5.0" solc-select use "0.5.0" slither-check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_1.txt 2>&1 @@ -181,6 +182,32 @@ then exit 255 fi +slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug_reinitializer --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_14.txt 2>&1 +DIFF=$(diff test_14.txt "$DIR_TESTS/test_14.txt") +if [ "$DIFF" != "" ] +then + echo "slither-check-upgradeability 14 failed" + cat test_14.txt + echo "" + cat "$DIR_TESTS/test_14.txt" + echo "" + echo "$DIFF" + exit 255 +fi + +slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_reinitializer_V2 --new-contract-name Counter_reinitializer_V3_V4 > test_15.txt 2>&1 +DIFF=$(diff test_15.txt "$DIR_TESTS/test_15.txt") +if [ "$DIFF" != "" ] +then + echo "slither-check-upgradeability 14 failed" + cat test_15.txt + echo "" + cat "$DIR_TESTS/test_15.txt" + echo "" + echo "$DIFF" + exit 255 +fi + rm test_1.txt rm test_2.txt rm test_3.txt @@ -194,3 +221,5 @@ rm test_10.txt rm test_11.txt rm test_12.txt rm test_13.txt +rm test_14.txt +rm test_15.txt diff --git a/setup.py b/setup.py index a669b82a3..ef9d81f20 100644 --- a/setup.py +++ b/setup.py @@ -8,16 +8,16 @@ setup( description="Slither is a Solidity and Vyper static analysis framework written in Python 3.", url="https://github.com/crytic/slither", author="Trail of Bits", - version="0.10.3", + version="0.10.4", packages=find_packages(), python_requires=">=3.8", install_requires=[ "packaging", - "prettytable>=3.3.0", + "prettytable>=3.10.2", "pycryptodome>=3.4.6", "crytic-compile>=0.3.7,<0.4.0", # "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", - "web3>=6.0.0", + "web3>=6.20.2, <7", "eth-abi>=4.0.0", "eth-typing>=3.0.0", "eth-utils>=2.1.0", diff --git a/slither/__main__.py b/slither/__main__.py index 58d276e1d..633ad68cf 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -14,7 +14,7 @@ from importlib import metadata from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union -from crytic_compile import cryticparser, CryticCompile, InvalidCompilation +from crytic_compile import cryticparser, CryticCompile from crytic_compile.platform.standard import generate_standard_export from crytic_compile.platform.etherscan import SUPPORTED_NETWORK from crytic_compile import compile_all, is_supported @@ -93,13 +93,7 @@ def process_all( detector_classes: List[Type[AbstractDetector]], printer_classes: List[Type[AbstractPrinter]], ) -> Tuple[List[Slither], List[Dict], List[Output], int]: - - try: - compilations = compile_all(target, **vars(args)) - except InvalidCompilation: - logger.error("Unable to compile all targets.") - sys.exit(2) - + compilations = compile_all(target, **vars(args)) slither_instances = [] results_detectors = [] results_printers = [] diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 7c5484431..44a168c2b 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,4 +97,5 @@ from .operations.incorrect_exp import IncorrectOperatorExponentiation from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable -from .statements.unused_import import UnusedImport + +# from .statements.unused_import import UnusedImport diff --git a/slither/detectors/attributes/incorrect_solc.py b/slither/detectors/attributes/incorrect_solc.py index 532a96493..e60412cef 100644 --- a/slither/detectors/attributes/incorrect_solc.py +++ b/slither/detectors/attributes/incorrect_solc.py @@ -71,7 +71,7 @@ Consider using the latest version of Solidity for testing.""" if op and op not in [">", ">=", "^"]: return self.LESS_THAN_TXT version_number = ".".join(version[2:]) - if version_number in bugs_by_version: + if version_number in bugs_by_version and len(bugs_by_version[version_number]): bugs = "\n".join([f"\t- {bug}" for bug in bugs_by_version[version_number]]) return self.BUGGY_VERSION_TXT + f"\n{bugs}" return None diff --git a/slither/detectors/functions/arbitrary_send_eth.py b/slither/detectors/functions/arbitrary_send_eth.py index f6c688a3f..56fb11250 100644 --- a/slither/detectors/functions/arbitrary_send_eth.py +++ b/slither/detectors/functions/arbitrary_send_eth.py @@ -30,6 +30,7 @@ from slither.slithir.operations import ( SolidityCall, Transfer, ) +from slither.core.variables.state_variable import StateVariable # pylint: disable=too-many-nested-blocks,too-many-branches from slither.utils.output import Output @@ -67,6 +68,8 @@ def arbitrary_send(func: Function) -> Union[bool, List[Node]]: continue if ir.call_value == SolidityVariableComposed("msg.value"): continue + if isinstance(ir.destination, StateVariable) and ir.destination.is_immutable: + continue if is_dependent( ir.call_value, SolidityVariableComposed("msg.value"), diff --git a/slither/detectors/functions/dead_code.py b/slither/detectors/functions/dead_code.py index 7a2c6dbc4..5cafa1650 100644 --- a/slither/detectors/functions/dead_code.py +++ b/slither/detectors/functions/dead_code.py @@ -25,7 +25,7 @@ class DeadCode(AbstractDetector): WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#dead-code" WIKI_TITLE = "Dead-code" - WIKI_DESCRIPTION = "Functions that are not sued." + WIKI_DESCRIPTION = "Functions that are not used." # region wiki_exploit_scenario WIKI_EXPLOIT_SCENARIO = """ diff --git a/slither/printers/summary/loc.py b/slither/printers/summary/loc.py index 35bb20fc4..886803e8e 100644 --- a/slither/printers/summary/loc.py +++ b/slither/printers/summary/loc.py @@ -17,8 +17,8 @@ from slither.utils.output import Output class LocPrinter(AbstractPrinter): ARGUMENT = "loc" - HELP = """Count the total number lines of code (LOC), source lines of code (SLOC), \ - and comment lines of code (CLOC) found in source files (SRC), dependencies (DEP), \ + HELP = """Count the total number lines of code (LOC), source lines of code (SLOC), + and comment lines of code (CLOC) found in source files (SRC), dependencies (DEP), and test files (TEST).""" WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#loc" diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 8a7ce3e1a..dea11676a 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -6,7 +6,7 @@ import shutil import sys import time from pathlib import Path -from typing import Type, List, Any, Optional +from typing import Type, List, Any, Optional, Union from crytic_compile import cryticparser from slither import Slither from slither.tools.mutator.utils.testing_generated_mutant import run_test_cmd @@ -116,7 +116,7 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: +def _get_mutators(mutators_list: Union[List[str], None]) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] if mutators_list is not None: detectors = [ diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 0bf0fb2a2..f86e7d3d4 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -2,7 +2,12 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator +from slither.core.variables.variable import Variable from slither.core.expressions.unary_operation import UnaryOperation +from slither.core.expressions.call_expression import CallExpression +from slither.core.expressions.member_access import MemberAccess +from slither.core.expressions.identifier import Identifier +from slither.core.solidity_types.array_type import ArrayType arithmetic_operators = [ BinaryType.ADDITION, @@ -27,7 +32,39 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods ir_expression = node.expression except: # pylint: disable=bare-except continue - for ir in node.irs: + + # Special cases handling .push and .pop on dynamic arrays. + # The IR for these operations has a binary operation due to internal conversion + # (see convert_to_push and convert_to_pop in slithir/convert.py) + # however it's not present in the source code and should not be mutated. + # pylint: disable=too-many-boolean-expressions + if ( + isinstance(ir_expression, CallExpression) + and isinstance(ir_expression.called, MemberAccess) + and ir_expression.called.member_name == "pop" + and isinstance(ir_expression.called.expression, Identifier) + and isinstance(ir_expression.called.expression.value, Variable) + and isinstance(ir_expression.called.expression.value.type, ArrayType) + ): + continue + + # For a .push instruction we skip the last 6 IR operations + # because they are fixed based on the internal conversion to the IR + # while we need to look at the preceding instructions because + # they might contain Binary IR to be mutated. + # For example for a.push(3+x) it's correct to mutate 3+x. + irs = ( + node.irs[:-6] + if isinstance(ir_expression, CallExpression) + and isinstance(ir_expression.called, MemberAccess) + and ir_expression.called.member_name == "push" + and isinstance(ir_expression.called.expression, Identifier) + and isinstance(ir_expression.called.expression.value, Variable) + and isinstance(ir_expression.called.expression.value.type, ArrayType) + else node.irs + ) + + for ir in irs: if isinstance(ir, Binary) and ir.type in arithmetic_operators: if isinstance(ir_expression, UnaryOperation): continue diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index cc58cbae1..fc621829f 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -31,7 +31,7 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods literal_replacements.append(variable.type.max) # append data type max value if str(variable.type).startswith("uint"): literal_replacements.append("1") - elif str(variable.type).startswith("uint"): + elif str(variable.type).startswith("int"): literal_replacements.append("-1") # Get the string start = variable.source_mapping.start @@ -63,7 +63,7 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods literal_replacements.append(variable.type.max) if str(variable.type).startswith("uint"): literal_replacements.append("1") - elif str(variable.type).startswith("uint"): + elif str(variable.type).startswith("int"): literal_replacements.append("-1") start = variable.source_mapping.start stop = start + variable.source_mapping.length diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 69c77a4ca..cb435f856 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,7 +1,7 @@ import abc import logging from pathlib import Path -from typing import Optional, Dict, Tuple, List +from typing import Optional, Dict, Tuple, List, Union from slither.core.compilation_unit import SlitherCompilationUnit from slither.formatters.utils.patches import apply_patch, create_diff from slither.tools.mutator.utils.testing_generated_mutant import test_patch @@ -27,7 +27,7 @@ class AbstractMutator( testing_command: str, testing_directory: str, contract_instance: Contract, - solc_remappings: str | None, + solc_remappings: Union[str, None], verbose: bool, very_verbose: bool, output_folder: Path, @@ -81,7 +81,7 @@ class AbstractMutator( (all_patches) = self._mutate() if "patches" not in all_patches: logger.debug("No patches found by %s", self.NAME) - return ([0, 0, 0], [0, 0, 0], self.dont_mutate_line) + return [0, 0, 0], [0, 0, 0], self.dont_mutate_line for file in all_patches["patches"]: # Note: This should only loop over a single file original_txt = self.slither.source_code[file].encode("utf8") @@ -146,4 +146,4 @@ class AbstractMutator( f"Found {self.uncaught_mutant_counts[2]} uncaught tweak mutants so far (out of {self.total_mutant_counts[2]} that compile)" ) - return (self.total_mutant_counts, self.uncaught_mutant_counts, self.dont_mutate_line) + return self.total_mutant_counts, self.uncaught_mutant_counts, self.dont_mutate_line diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index 7c02ce099..81e30efc6 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -111,7 +111,7 @@ def get_sol_file_list(codebase: Path, ignore_paths: Union[List[str], None]) -> L # if input is folder if codebase.is_dir(): for file_name in codebase.rglob("*.sol"): - if not any(part in ignore_paths for part in file_name.parts): + if file_name.is_file() and not any(part in ignore_paths for part in file_name.parts): sol_file_list.append(file_name.as_posix()) return sol_file_list diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 39e7d39de..d62fc3ff0 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -22,7 +22,12 @@ def compile_generated_mutant(file_path: str, mappings: str) -> bool: return False -def run_test_cmd(cmd: str, timeout: int | None, target_file: str | None, verbose: bool) -> bool: +def run_test_cmd( + cmd: str, + timeout: Union[int, None] = None, + target_file: Union[str, None] = None, + verbose: bool = False, +) -> bool: """ function to run codebase tests returns: boolean whether the tests passed or not diff --git a/slither/tools/upgradeability/checks/all_checks.py b/slither/tools/upgradeability/checks/all_checks.py index 2289c3808..1581ab530 100644 --- a/slither/tools/upgradeability/checks/all_checks.py +++ b/slither/tools/upgradeability/checks/all_checks.py @@ -7,6 +7,7 @@ from slither.tools.upgradeability.checks.initialization import ( MissingCalls, MultipleCalls, InitializeTarget, + MultipleReinitializers, ) from slither.tools.upgradeability.checks.functions_ids import IDCollision, FunctionShadowing diff --git a/slither/tools/upgradeability/checks/initialization.py b/slither/tools/upgradeability/checks/initialization.py index 2055a322a..e31a71947 100644 --- a/slither/tools/upgradeability/checks/initialization.py +++ b/slither/tools/upgradeability/checks/initialization.py @@ -7,6 +7,7 @@ from slither.tools.upgradeability.checks.abstract_checks import ( CheckClassification, ) from slither.utils.colors import red +from slither.exceptions import SlitherError logger = logging.getLogger("Slither-check-upgradeability") @@ -18,7 +19,7 @@ class MultipleInitTarget(Exception): def _has_initialize_modifier(function: Function): if not function.modifiers: return False - return any((m.name == "initializer") for m in function.modifiers) + return any((m.name in ("initializer", "reinitializer")) for m in function.modifiers) def _get_initialize_functions(contract): @@ -164,7 +165,7 @@ class MissingInitializerModifier(AbstractCheck): # region wiki_description WIKI_DESCRIPTION = """ -Detect if `Initializable.initializer()` is called. +Detect if `Initializable.initializer()` or `Initializable.reinitializer(uint64)` is called. """ # endregion wiki_description @@ -184,7 +185,7 @@ contract Contract{ # region wiki_recommendation WIKI_RECOMMENDATION = """ -Use `Initializable.initializer()`. +Use `Initializable.initializer()` or `Initializable.reinitializer(uint64)`. """ # endregion wiki_recommendation @@ -199,15 +200,18 @@ Use `Initializable.initializer()`. if initializable not in self.contract.inheritance: return [] initializer = self.contract.get_modifier_from_canonical_name("Initializable.initializer()") + reinitializer = self.contract.get_modifier_from_canonical_name( + "Initializable.reinitializer(uint64)" + ) # InitializableInitializer - if initializer is None: + if initializer is None and reinitializer is None: return [] results = [] all_init_functions = _get_initialize_functions(self.contract) for f in all_init_functions: - if initializer not in f.modifiers: - info = [f, " does not call the initializer modifier.\n"] + if initializer not in f.modifiers and reinitializer not in f.modifiers: + info = [f, " does not call the initializer or reinitializer modifier.\n"] json = self.generate_result(info) results.append(json) return results @@ -271,6 +275,9 @@ Ensure all the initialize functions are reached by the most derived initialize f all_init_functions_called = _get_all_internal_calls(most_derived_init) + [most_derived_init] missing_calls = [f for f in all_init_functions if not f in all_init_functions_called] for f in missing_calls: + # we don't account reinitializers + if any((m.name == "reinitializer") for m in f.modifiers): + continue info = ["Missing call to ", f, " in ", most_derived_init, ".\n"] json = self.generate_result(info) results.append(json) @@ -396,3 +403,106 @@ Ensure that the function is called at deployment. ] json = self.generate_result(info) return [json] + + +class MultipleReinitializers(AbstractCheck): + ARGUMENT = "multiple-new-reinitializers" + IMPACT = CheckClassification.LOW + + HELP = "Multiple new reinitializers in the updated contract" + WIKI = ( + "https://github.com/crytic/slither/wiki/Upgradeability-Checks#multiple-new-reinitializers" + ) + WIKI_TITLE = "Multiple new reinitializers in the updated contract" + + # region wiki_description + WIKI_DESCRIPTION = """ +Detect multiple new reinitializers in the updated contract`. +""" + # endregion wiki_description + + # region wiki_exploit_scenario + WIKI_EXPLOIT_SCENARIO = """ +```solidity +contract Counter is Initializable { + uint256 public x; + + function initialize(uint256 _x) public initializer { + x = _x; + } + + function changeX() public { + x++; + } +} + +contract CounterV2 is Initializable { + uint256 public x; + uint256 public y; + uint256 public z; + + function initializeV2(uint256 _y) public reinitializer(2) { + y = _y; + } + + function initializeV3(uint256 _z) public reinitializer(3) { + z = _z; + } + + function changeX() public { + x = x + y + z; + } +} +``` +`CounterV2` has two reinitializers with new versions `2` and `3`. If `initializeV3()` is called first, the `initializeV2()` can't be called to initialize `y`, which may brings security risks. +""" + # endregion wiki_exploit_scenario + + # region wiki_recommendation + WIKI_RECOMMENDATION = """ +Do not use multiple reinitializers with higher versions in the updated contract. Please consider combining new reinitializers into a single one. +""" + # endregion wiki_recommendation + + REQUIRE_CONTRACT = True + REQUIRE_CONTRACT_V2 = True + + def _check(self): + contract_v1 = self.contract + contract_v2 = self.contract_v2 + + if contract_v2 is None: + raise SlitherError("multiple-new-reinitializers requires a V2 contract") + + initializerV1 = contract_v1.get_modifier_from_canonical_name("Initializable.initializer()") + reinitializerV1 = contract_v1.get_modifier_from_canonical_name( + "Initializable.reinitializer(uint64)" + ) + reinitializerV2 = contract_v2.get_modifier_from_canonical_name( + "Initializable.reinitializer(uint64)" + ) + + # contractV1 has initializer or reinitializer + if initializerV1 is None and reinitializerV1 is None: + return [] + # contractV2 has reinitializer + if reinitializerV2 is None: + return [] + + initializer_funcs_v1 = _get_initialize_functions(contract_v1) + initializer_funcs_v2 = _get_initialize_functions(contract_v2) + new_reinitializer_funcs = [] + for fv2 in initializer_funcs_v2: + if not any((fv2.full_name == fv1.full_name) for fv1 in initializer_funcs_v1): + new_reinitializer_funcs.append(fv2) + + results = [] + if len(new_reinitializer_funcs) > 1: + for f in new_reinitializer_funcs: + info = [ + f, + " multiple new reinitializers which should be combined into one per upgrade.\n", + ] + json = self.generate_result(info) + results.append(json) + return results diff --git a/slither/utils/command_line.py b/slither/utils/command_line.py index f5b9ab452..b8888cb85 100644 --- a/slither/utils/command_line.py +++ b/slither/utils/command_line.py @@ -360,8 +360,10 @@ def output_printers(printer_classes: List[Type[AbstractPrinter]]) -> None: printers_list = sorted(printers_list, key=lambda element: (element[0])) idx = 1 for (argument, help_info) in printers_list: - table.add_row([str(idx), argument, help_info]) + # Clean multi line HELP info + table.add_row([str(idx), argument, " ".join(x.strip() for x in help_info.splitlines())]) idx = idx + 1 + print(table) diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index b33fb9c5f..ac666a501 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -1,3 +1,4 @@ +from shutil import get_terminal_size from typing import List, Dict, Union from prettytable import PrettyTable @@ -7,7 +8,12 @@ from slither.utils.colors import Colors class MyPrettyTable: - def __init__(self, field_names: List[str], pretty_align: bool = True): # TODO: True by default? + def __init__( + self, + field_names: List[str], + pretty_align: bool = True, + max_width: Union[int, None] = "max", # Default value is "max" + ): self._field_names = field_names self._rows: List = [] self._options: Dict = {} @@ -19,6 +25,17 @@ class MyPrettyTable: else: self._options["set_alignment"] = [] + self.max_width = None + if max_width == "max": + # We use (0,0) as a fallback to detect if we are not attached to a terminal + # In this case, we fall back to the default behavior (i.e. printing as much as possible) + terminal_column = get_terminal_size((0, 0)).columns + if terminal_column != 0: + # We reduce slightly the max-width to take into account inconsistencies in terminals + self.max_width = terminal_column - 3 + else: + self.max_width = max_width + def add_row(self, row: List[Union[str, List[str]]]) -> None: self._rows.append(row) @@ -28,6 +45,9 @@ class MyPrettyTable: else: table = PrettyTable(self._field_names) + if self.max_width is not None: + table.max_table_width = self.max_width + for row in self._rows: table.add_row(row) if len(self._options["set_alignment"]): @@ -63,7 +83,5 @@ def make_pretty_table( table_row = [row] + [body[row][key] for key in headers[1:]] table.add_row(table_row) if totals: - table.add_row( - [total_header] + [sum([body[row][key] for row in body]) for key in headers[1:]] - ) + table.add_row([total_header] + [sum(body[row][key] for row in body) for key in headers[1:]]) return table diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_6_11_arbitrary_send_eth_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_6_11_arbitrary_send_eth_sol__0.txt index 56ccf3cd3..d2a1a487f 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_6_11_arbitrary_send_eth_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_6_11_arbitrary_send_eth_sol__0.txt @@ -1,8 +1,8 @@ -Test.indirect() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#19-21) sends eth to arbitrary user +Test.direct() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#16-18) sends eth to arbitrary user Dangerous calls: - - destination.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#20) + - msg.sender.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#17) -Test.direct() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#11-13) sends eth to arbitrary user +Test.indirect() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#24-26) sends eth to arbitrary user Dangerous calls: - - msg.sender.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#12) + - destination.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol#25) diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_7_6_arbitrary_send_eth_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_7_6_arbitrary_send_eth_sol__0.txt index 3d4a4f037..5d9356126 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_7_6_arbitrary_send_eth_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_ArbitrarySendEth_0_7_6_arbitrary_send_eth_sol__0.txt @@ -1,8 +1,8 @@ -Test.direct() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#11-13) sends eth to arbitrary user +Test.direct() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#16-18) sends eth to arbitrary user Dangerous calls: - - msg.sender.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#12) + - msg.sender.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#17) -Test.indirect() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#19-21) sends eth to arbitrary user +Test.indirect() (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#24-26) sends eth to arbitrary user Dangerous calls: - - destination.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#20) + - destination.send(address(this).balance) (tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol#25) diff --git a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol index 9a0c743c4..d494d0e08 100644 --- a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol +++ b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol @@ -1,13 +1,18 @@ contract Test{ address payable destination; - + address payable immutable destination_imm; mapping (address => uint) balances; constructor() public{ + destination_imm = payable(msg.sender); balances[msg.sender] = 0; } + function send_immutable() public{ + destination_imm.send(address(this).balance); + } + function direct() public{ msg.sender.send(address(this).balance); } diff --git a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol-0.6.11.zip b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol-0.6.11.zip index ebbb8c346..bec986a85 100644 Binary files a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol-0.6.11.zip and b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.6.11/arbitrary_send_eth.sol-0.6.11.zip differ diff --git a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol index 9a0c743c4..d494d0e08 100644 --- a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol +++ b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol @@ -1,13 +1,18 @@ contract Test{ address payable destination; - + address payable immutable destination_imm; mapping (address => uint) balances; constructor() public{ + destination_imm = payable(msg.sender); balances[msg.sender] = 0; } + function send_immutable() public{ + destination_imm.send(address(this).balance); + } + function direct() public{ msg.sender.send(address(this).balance); } diff --git a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol-0.7.6.zip b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol-0.7.6.zip index 3bd2416f5..e84aca40e 100644 Binary files a/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol-0.7.6.zip and b/tests/e2e/detectors/test_data/arbitrary-send-eth/0.7.6/arbitrary_send_eth.sol-0.7.6.zip differ diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 299f2ea03..2c6a5f55a 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1714,191 +1714,191 @@ ALL_TESTS = [ "out_of_order_retryable.sol", "0.8.20", ), - Test( - all_detectors.UnusedImport, - "ConstantContractLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "ConstantContractLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "ConstantTopLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "ConstantTopLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "ContractUsedInContractTest1.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "ContractUsedInContractTest2.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "ContractUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomErrorTopLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomEventContractLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomEventContractLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeContractLevelUsedInContractTest1.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeContractLevelUsedInContractTest2.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeContractLevelUsedInContractTest3.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeContractLevelUsedInContractTest4.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeContractLevelUsedTopLevelTest1.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeContractLevelUsedTopLevelTest2.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeTopLevelUsedInContractTest1.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeTopLevelUsedInContractTest2.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeTopLevelUsedInContractTest3.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeTopLevelUsedInContractTest4.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeTopLevelUsedTopLevelTest1.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "CustomTypeTopLevelUsedTopLevelTest2.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "EnumContractLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "EnumContractLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "EnumTopLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "EnumTopLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "FunctionContractLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "FunctionContractLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "FunctionTopLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "FunctionTopLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "LibraryUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "LibraryUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "StructContractLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "StructContractLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "StructTopLevelUsedInContractTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "StructTopLevelUsedTopLevelTest.sol", - "0.8.16", - ), - Test( - all_detectors.UnusedImport, - "C.sol", - "0.8.16", - ), + # Test( + # all_detectors.UnusedImport, + # "ConstantContractLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "ConstantContractLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "ConstantTopLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "ConstantTopLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "ContractUsedInContractTest1.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "ContractUsedInContractTest2.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "ContractUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomErrorTopLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomEventContractLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomEventContractLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeContractLevelUsedInContractTest1.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeContractLevelUsedInContractTest2.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeContractLevelUsedInContractTest3.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeContractLevelUsedInContractTest4.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeContractLevelUsedTopLevelTest1.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeContractLevelUsedTopLevelTest2.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeTopLevelUsedInContractTest1.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeTopLevelUsedInContractTest2.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeTopLevelUsedInContractTest3.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeTopLevelUsedInContractTest4.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeTopLevelUsedTopLevelTest1.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "CustomTypeTopLevelUsedTopLevelTest2.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "EnumContractLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "EnumContractLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "EnumTopLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "EnumTopLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "FunctionContractLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "FunctionContractLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "FunctionTopLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "FunctionTopLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "LibraryUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "LibraryUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "StructContractLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "StructContractLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "StructTopLevelUsedInContractTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "StructTopLevelUsedTopLevelTest.sol", + # "0.8.16", + # ), + # Test( + # all_detectors.UnusedImport, + # "C.sol", + # "0.8.16", + # ), ] GENERIC_PATH = "/GENERIC_PATH" diff --git a/tests/e2e/test_cli.py b/tests/e2e/test_cli.py deleted file mode 100644 index 72e0441d6..000000000 --- a/tests/e2e/test_cli.py +++ /dev/null @@ -1,20 +0,0 @@ -import sys -import tempfile -import pytest - -from slither.__main__ import main_impl - - -def test_cli_exit_on_invalid_compilation_file(caplog): - - with tempfile.NamedTemporaryFile("w") as f: - f.write("pragma solidity ^0.10000.0;") - - sys.argv = ["slither", f.name] - with pytest.raises(SystemExit) as pytest_wrapped_e: - main_impl([], []) - - assert pytest_wrapped_e.type == SystemExit - assert pytest_wrapped_e.value.code == 2 - - assert caplog.record_tuples[0] == ("Slither", 40, "Unable to compile all targets.") diff --git a/tests/tools/check_upgradeability/contract_initialization.sol b/tests/tools/check_upgradeability/contract_initialization.sol index d17125ee9..ab6ab8e51 100644 --- a/tests/tools/check_upgradeability/contract_initialization.sol +++ b/tests/tools/check_upgradeability/contract_initialization.sol @@ -6,6 +6,10 @@ contract Initializable{ _; } + modifier reinitializer(uint64 version){ + _; + } + } contract Contract_no_bug is Initializable{ @@ -16,6 +20,14 @@ contract Contract_no_bug is Initializable{ } +contract Contract_no_bug_reinitializer is Initializable{ + + function initialize() public reinitializer(2){ + + } + +} + contract Contract_lack_to_call_modifier is Initializable{ function initialize() public { @@ -47,3 +59,45 @@ contract Contract_double_call is Contract_no_bug, Contract_no_bug_inherits{ } } + +contract Contract_reinitializer_V2 is Initializable { + uint256 public x; + + function initialize(uint256 _x) public initializer { + x = _x; + } + + function initializeV2(uint256 _x) public reinitializer(2) { + x = _x; + } + + function changeX() public { + x++; + } +} + +contract Counter_reinitializer_V3_V4 is Initializable { + uint256 public x; + uint256 public y; + uint256 public z; + + function initialize(uint256 _x) public initializer { + x = _x; + } + + function initializeV2(uint256 _x) public reinitializer(2) { + x = _x; + } + + function initializeV3(uint256 _y) public reinitializer(3) { + y = _y; + } + + function initializeV4(uint256 _z) public reinitializer(4) { + z = _z; + } + + function changeX() public { + x = x + y + z; + } +} \ No newline at end of file diff --git a/tests/tools/check_upgradeability/test_10.txt b/tests/tools/check_upgradeability/test_10.txt index 3d317aca5..9a7e022c1 100644 --- a/tests/tools/check_upgradeability/test_10.txt +++ b/tests/tools/check_upgradeability/test_10.txt @@ -2,12 +2,12 @@ INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing INFO:Slither: -ContractV1.destination (tests/check-upgradeability/contractV1.sol#2) was not constant but ContractV2.destination (tests/check-upgradeability/contract_v2_constant.sol#2) is. +ContractV1.destination (tests/tools/check_upgradeability/contractV1.sol#2) was not constant but ContractV2.destination (tests/tools/check_upgradeability/contract_v2_constant.sol#2) is. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#variables-that-should-not-be-constant INFO:Slither: -Variable missing in ContractV2 (tests/check-upgradeability/contract_v2_constant.sol#1-3): ContractV1.destination (tests/check-upgradeability/contractV1.sol#2) +Variable missing in ContractV2 (tests/tools/check_upgradeability/contract_v2_constant.sol#1-3): ContractV1.destination (tests/tools/check_upgradeability/contractV1.sol#2) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#missing-variables INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing -INFO:Slither:4 findings, 21 detectors run +INFO:Slither:4 findings, 22 detectors run diff --git a/tests/tools/check_upgradeability/test_11.txt b/tests/tools/check_upgradeability/test_11.txt index e2ad677b1..d942ce6b7 100644 --- a/tests/tools/check_upgradeability/test_11.txt +++ b/tests/tools/check_upgradeability/test_11.txt @@ -2,6 +2,6 @@ INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing INFO:Slither: -ContractV1.destination (tests/check-upgradeability/contract_v1_var_init.sol#2) is a state variable with an initial value. +ContractV1.destination (tests/tools/check_upgradeability/contract_v1_var_init.sol#2) is a state variable with an initial value. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#state-variable-initialized INFO:Slither:2 findings, 8 detectors run diff --git a/tests/tools/check_upgradeability/test_12.txt b/tests/tools/check_upgradeability/test_12.txt index 353d8ebdb..7641e8335 100644 --- a/tests/tools/check_upgradeability/test_12.txt +++ b/tests/tools/check_upgradeability/test_12.txt @@ -4,4 +4,4 @@ Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initiali INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing -INFO:Slither:2 findings, 21 detectors run +INFO:Slither:2 findings, 22 detectors run diff --git a/tests/tools/check_upgradeability/test_13.txt b/tests/tools/check_upgradeability/test_13.txt index 9635f9a43..e39b9b1ac 100644 --- a/tests/tools/check_upgradeability/test_13.txt +++ b/tests/tools/check_upgradeability/test_13.txt @@ -2,11 +2,11 @@ INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing INFO:Slither: -Different variables between ContractV1 (tests/check-upgradeability/contractV1_struct.sol#1-8) and ContractV2 (tests/check-upgradeability/contractV2_struct_bug.sol#1-8) - ContractV1.foo (tests/check-upgradeability/contractV1_struct.sol#7) - ContractV2.foo (tests/check-upgradeability/contractV2_struct_bug.sol#7) +Different variables between ContractV1 (tests/tools/check_upgradeability/contractV1_struct.sol#1-8) and ContractV2 (tests/tools/check_upgradeability/contractV2_struct_bug.sol#1-8) + ContractV1.foo (tests/tools/check_upgradeability/contractV1_struct.sol#7) + ContractV2.foo (tests/tools/check_upgradeability/contractV2_struct_bug.sol#7) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#incorrect-variables-with-the-v2 INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing -INFO:Slither:3 findings, 21 detectors run +INFO:Slither:3 findings, 22 detectors run diff --git a/tests/tools/check_upgradeability/test_14.txt b/tests/tools/check_upgradeability/test_14.txt new file mode 100644 index 000000000..da412418e --- /dev/null +++ b/tests/tools/check_upgradeability/test_14.txt @@ -0,0 +1,4 @@ +INFO:Slither: +Contract_no_bug_reinitializer (tests/tools/check_upgradeability/contract_initialization.sol#23-29) needs to be initialized by Contract_no_bug_reinitializer.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#25-27). +Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function +INFO:Slither:1 findings, 12 detectors run diff --git a/tests/tools/check_upgradeability/test_15.txt b/tests/tools/check_upgradeability/test_15.txt new file mode 100644 index 000000000..d9c588f73 --- /dev/null +++ b/tests/tools/check_upgradeability/test_15.txt @@ -0,0 +1,15 @@ +INFO:Slither: +Contract_reinitializer_V2 (tests/tools/check_upgradeability/contract_initialization.sol#63-77) needs to be initialized by Contract_reinitializer_V2.initialize(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#66-68). +Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function +INFO:Slither: +Extra variables in Counter_reinitializer_V3_V4 (tests/tools/check_upgradeability/contract_initialization.sol#79-104): Counter_reinitializer_V3_V4.y (tests/tools/check_upgradeability/contract_initialization.sol#81) +Extra variables in Counter_reinitializer_V3_V4 (tests/tools/check_upgradeability/contract_initialization.sol#79-104): Counter_reinitializer_V3_V4.z (tests/tools/check_upgradeability/contract_initialization.sol#82) +Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#extra-variables-in-the-v2 +INFO:Slither: +Counter_reinitializer_V3_V4.initializeV3(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#92-94) multiple new reinitializers which should be combined into one per upgrade. +Counter_reinitializer_V3_V4.initializeV4(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#96-98) multiple new reinitializers which should be combined into one per upgrade. +Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#multiple-new-reinitializers +INFO:Slither: +Counter_reinitializer_V3_V4 (tests/tools/check_upgradeability/contract_initialization.sol#79-104) needs to be initialized by Counter_reinitializer_V3_V4.initialize(uint256) (tests/tools/check_upgradeability/contract_initialization.sol#84-86). +Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function +INFO:Slither:6 findings, 22 detectors run diff --git a/tests/tools/check_upgradeability/test_2.txt b/tests/tools/check_upgradeability/test_2.txt index dcf910c00..a3970ffc1 100644 --- a/tests/tools/check_upgradeability/test_2.txt +++ b/tests/tools/check_upgradeability/test_2.txt @@ -4,4 +4,4 @@ Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initiali INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing -INFO:Slither:2 findings, 25 detectors run +INFO:Slither:2 findings, 26 detectors run diff --git a/tests/tools/check_upgradeability/test_3.txt b/tests/tools/check_upgradeability/test_3.txt index fb694d5fb..5cdc9fc7e 100644 --- a/tests/tools/check_upgradeability/test_3.txt +++ b/tests/tools/check_upgradeability/test_3.txt @@ -2,22 +2,22 @@ INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing INFO:Slither: -Different variables between ContractV2 (tests/check-upgradeability/contractV2_bug.sol#1-5) and Proxy (tests/check-upgradeability/proxy.sol#7-27) - ContractV2.destination (tests/check-upgradeability/contractV2_bug.sol#2) - Proxy.destination (tests/check-upgradeability/proxy.sol#9) +Different variables between ContractV2 (tests/tools/check_upgradeability/contractV2_bug.sol#1-5) and Proxy (tests/tools/check_upgradeability/proxy.sol#7-27) + ContractV2.destination (tests/tools/check_upgradeability/contractV2_bug.sol#2) + Proxy.destination (tests/tools/check_upgradeability/proxy.sol#9) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#incorrect-variables-with-the-proxy INFO:Slither: -Function shadowing found: ContractV2.myFunc (tests/check-upgradeability/contractV2_bug.sol#4) Proxy.myFunc() (tests/check-upgradeability/proxy.sol#11) +Function shadowing found: ContractV2.myFunc (tests/tools/check_upgradeability/contractV2_bug.sol#4) Proxy.myFunc() (tests/tools/check_upgradeability/proxy.sol#11) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#functions-shadowing INFO:Slither: -Different variables between ContractV1 (tests/check-upgradeability/contractV1.sol#1-3) and ContractV2 (tests/check-upgradeability/contractV2_bug.sol#1-5) - ContractV1.destination (tests/check-upgradeability/contractV1.sol#2) - ContractV2.destination (tests/check-upgradeability/contractV2_bug.sol#2) +Different variables between ContractV1 (tests/tools/check_upgradeability/contractV1.sol#1-3) and ContractV2 (tests/tools/check_upgradeability/contractV2_bug.sol#1-5) + ContractV1.destination (tests/tools/check_upgradeability/contractV1.sol#2) + ContractV2.destination (tests/tools/check_upgradeability/contractV2_bug.sol#2) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#incorrect-variables-with-the-v2 INFO:Slither: -Extra variables in ContractV2 (tests/check-upgradeability/contractV2_bug.sol#1-5): ContractV2.myFunc (tests/check-upgradeability/contractV2_bug.sol#4) +Extra variables in ContractV2 (tests/tools/check_upgradeability/contractV2_bug.sol#1-5): ContractV2.myFunc (tests/tools/check_upgradeability/contractV2_bug.sol#4) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#extra-variables-in-the-v2 INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing -INFO:Slither:6 findings, 25 detectors run +INFO:Slither:6 findings, 26 detectors run diff --git a/tests/tools/check_upgradeability/test_4.txt b/tests/tools/check_upgradeability/test_4.txt index 4752eb706..eb088324d 100644 --- a/tests/tools/check_upgradeability/test_4.txt +++ b/tests/tools/check_upgradeability/test_4.txt @@ -2,19 +2,19 @@ INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing INFO:Slither: -Different variables between ContractV2 (tests/check-upgradeability/contractV2_bug2.sol#4-6) and Proxy (tests/check-upgradeability/proxy.sol#7-27) - Base.val (tests/check-upgradeability/contractV2_bug2.sol#2) - Proxy.destination (tests/check-upgradeability/proxy.sol#9) +Different variables between ContractV2 (tests/tools/check_upgradeability/contractV2_bug2.sol#4-6) and Proxy (tests/tools/check_upgradeability/proxy.sol#7-27) + Base.val (tests/tools/check_upgradeability/contractV2_bug2.sol#2) + Proxy.destination (tests/tools/check_upgradeability/proxy.sol#9) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#incorrect-variables-with-the-proxy INFO:Slither: -Different variables between ContractV1 (tests/check-upgradeability/contractV1.sol#1-3) and ContractV2 (tests/check-upgradeability/contractV2_bug2.sol#4-6) - ContractV1.destination (tests/check-upgradeability/contractV1.sol#2) - Base.val (tests/check-upgradeability/contractV2_bug2.sol#2) +Different variables between ContractV1 (tests/tools/check_upgradeability/contractV1.sol#1-3) and ContractV2 (tests/tools/check_upgradeability/contractV2_bug2.sol#4-6) + ContractV1.destination (tests/tools/check_upgradeability/contractV1.sol#2) + Base.val (tests/tools/check_upgradeability/contractV2_bug2.sol#2) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#incorrect-variables-with-the-v2 INFO:Slither: -Extra variables in ContractV2 (tests/check-upgradeability/contractV2_bug2.sol#4-6): ContractV2.destination (tests/check-upgradeability/contractV2_bug2.sol#5) +Extra variables in ContractV2 (tests/tools/check_upgradeability/contractV2_bug2.sol#4-6): ContractV2.destination (tests/tools/check_upgradeability/contractV2_bug2.sol#5) Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#extra-variables-in-the-v2 INFO:Slither: Initializable contract not found, the contract does not follow a standard initalization schema. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializable-is-missing -INFO:Slither:5 findings, 25 detectors run +INFO:Slither:5 findings, 26 detectors run diff --git a/tests/tools/check_upgradeability/test_5.txt b/tests/tools/check_upgradeability/test_5.txt index 805602ee4..da889fb59 100644 --- a/tests/tools/check_upgradeability/test_5.txt +++ b/tests/tools/check_upgradeability/test_5.txt @@ -1,4 +1,4 @@ INFO:Slither: -Contract_no_bug (tests/check-upgradeability/contract_initialization.sol#11-17) needs to be initialized by Contract_no_bug.initialize() (tests/check-upgradeability/contract_initialization.sol#13-15). +Contract_no_bug (tests/tools/check_upgradeability/contract_initialization.sol#15-21) needs to be initialized by Contract_no_bug.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#17-19). Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function INFO:Slither:1 findings, 12 detectors run diff --git a/tests/tools/check_upgradeability/test_6.txt b/tests/tools/check_upgradeability/test_6.txt index 663cb62d0..80b8d8b51 100644 --- a/tests/tools/check_upgradeability/test_6.txt +++ b/tests/tools/check_upgradeability/test_6.txt @@ -1,7 +1,7 @@ INFO:Slither: -Contract_lack_to_call_modifier (tests/check-upgradeability/contract_initialization.sol#19-24) needs to be initialized by Contract_lack_to_call_modifier.initialize() (tests/check-upgradeability/contract_initialization.sol#21-23). +Contract_lack_to_call_modifier (tests/tools/check_upgradeability/contract_initialization.sol#31-36) needs to be initialized by Contract_lack_to_call_modifier.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#33-35). Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function INFO:Slither: -Contract_lack_to_call_modifier.initialize() (tests/check-upgradeability/contract_initialization.sol#21-23) does not call the initializer modifier. +Contract_lack_to_call_modifier.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#33-35) does not call the initializer or reinitializer modifier. Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initializer-is-not-called INFO:Slither:2 findings, 12 detectors run diff --git a/tests/tools/check_upgradeability/test_7.txt b/tests/tools/check_upgradeability/test_7.txt index 9f232338e..02607207b 100644 --- a/tests/tools/check_upgradeability/test_7.txt +++ b/tests/tools/check_upgradeability/test_7.txt @@ -1,7 +1,7 @@ INFO:Slither: -Contract_not_called_super_init (tests/check-upgradeability/contract_initialization.sol#26-32) needs to be initialized by Contract_not_called_super_init.initialize() (tests/check-upgradeability/contract_initialization.sol#28-30). +Contract_not_called_super_init (tests/tools/check_upgradeability/contract_initialization.sol#38-44) needs to be initialized by Contract_not_called_super_init.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#40-42). Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function INFO:Slither: -Missing call to Contract_no_bug.initialize() (tests/check-upgradeability/contract_initialization.sol#13-15) in Contract_not_called_super_init.initialize() (tests/check-upgradeability/contract_initialization.sol#28-30). +Missing call to Contract_no_bug.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#17-19) in Contract_not_called_super_init.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#40-42). Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-functions-are-not-called INFO:Slither:2 findings, 12 detectors run diff --git a/tests/tools/check_upgradeability/test_8.txt b/tests/tools/check_upgradeability/test_8.txt index 38c71e28c..8cd703bea 100644 --- a/tests/tools/check_upgradeability/test_8.txt +++ b/tests/tools/check_upgradeability/test_8.txt @@ -1,4 +1,4 @@ INFO:Slither: -Contract_no_bug_inherits (tests/check-upgradeability/contract_initialization.sol#34-40) needs to be initialized by Contract_no_bug_inherits.initialize() (tests/check-upgradeability/contract_initialization.sol#36-38). +Contract_no_bug_inherits (tests/tools/check_upgradeability/contract_initialization.sol#46-52) needs to be initialized by Contract_no_bug_inherits.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#48-50). Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function INFO:Slither:1 findings, 12 detectors run diff --git a/tests/tools/check_upgradeability/test_9.txt b/tests/tools/check_upgradeability/test_9.txt index a67578a08..cece4f6ea 100644 --- a/tests/tools/check_upgradeability/test_9.txt +++ b/tests/tools/check_upgradeability/test_9.txt @@ -1,7 +1,7 @@ INFO:Slither: -Contract_double_call (tests/check-upgradeability/contract_initialization.sol#42-49) needs to be initialized by Contract_double_call.initialize() (tests/check-upgradeability/contract_initialization.sol#44-47). +Contract_double_call (tests/tools/check_upgradeability/contract_initialization.sol#54-61) needs to be initialized by Contract_double_call.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#56-59). Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-function INFO:Slither: -Contract_no_bug.initialize() (tests/check-upgradeability/contract_initialization.sol#13-15) is called multiple times in Contract_double_call.initialize() (tests/check-upgradeability/contract_initialization.sol#44-47). +Contract_no_bug.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#17-19) is called multiple times in Contract_double_call.initialize() (tests/tools/check_upgradeability/contract_initialization.sol#56-59). Reference: https://github.com/crytic/slither/wiki/Upgradeability-Checks#initialize-functions-are-called-multiple-times INFO:Slither:2 findings, 12 detectors run diff --git a/tests/tools/mutator/__init__.py b/tests/tools/mutator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tools/mutator/test_data/test_source_unit/README.md b/tests/tools/mutator/test_data/test_source_unit/README.md new file mode 100644 index 000000000..554472962 --- /dev/null +++ b/tests/tools/mutator/test_data/test_source_unit/README.md @@ -0,0 +1,7 @@ +# Counter + +Init using : + +```shell +forge install --no-commit --no-git . +``` diff --git a/tests/tools/mutator/test_data/test_source_unit/foundry.toml b/tests/tools/mutator/test_data/test_source_unit/foundry.toml new file mode 100644 index 000000000..908c595d0 --- /dev/null +++ b/tests/tools/mutator/test_data/test_source_unit/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] +solc = "0.8.15" + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/tests/tools/mutator/test_data/test_source_unit/script/Counter.s.sol b/tests/tools/mutator/test_data/test_source_unit/script/Counter.s.sol new file mode 100644 index 000000000..df9ee8b02 --- /dev/null +++ b/tests/tools/mutator/test_data/test_source_unit/script/Counter.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + } +} diff --git a/tests/tools/mutator/test_data/test_source_unit/src/Counter.sol b/tests/tools/mutator/test_data/test_source_unit/src/Counter.sol new file mode 100644 index 000000000..7a83fefc4 --- /dev/null +++ b/tests/tools/mutator/test_data/test_source_unit/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/tests/tools/mutator/test_data/test_source_unit/test/Counter.t.sol b/tests/tools/mutator/test_data/test_source_unit/test/Counter.t.sol new file mode 100644 index 000000000..6178bf4d2 --- /dev/null +++ b/tests/tools/mutator/test_data/test_source_unit/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} diff --git a/tests/tools/mutator/test_mutator.py b/tests/tools/mutator/test_mutator.py new file mode 100644 index 000000000..68b595319 --- /dev/null +++ b/tests/tools/mutator/test_mutator.py @@ -0,0 +1,133 @@ +import argparse +from contextlib import contextmanager +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +from unittest import mock + +import pytest +from slither import Slither +from slither.tools.mutator.__main__ import _get_mutators, main +from slither.tools.mutator.utils.testing_generated_mutant import run_test_cmd +from slither.tools.mutator.utils.file_handling import get_sol_file_list, backup_source_file + + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" + +foundry_available = shutil.which("forge") is not None +project_ready = Path(TEST_DATA_DIR, "test_source_unit/lib/forge-std").exists() + + +@contextmanager +def change_directory(new_dir): + original_dir = os.getcwd() + os.chdir(new_dir) + try: + yield + finally: + os.chdir(original_dir) + + +def test_get_mutators(): + + mutators = _get_mutators(None) + assert mutators + + mutators = _get_mutators(["ASOR"]) + assert len(mutators) == 1 + assert mutators[0].NAME == "ASOR" + + mutators = _get_mutators(["ASOR", "NotExisiting"]) + assert len(mutators) == 1 + + +@mock.patch( + "argparse.ArgumentParser.parse_args", + return_value=argparse.Namespace( + test_cmd="forge test", + test_dir=None, + ignore_dirs="lib,mutation_campaign", + output_dir=None, + timeout=None, + solc_remaps="forge-std=./lib/forge-std", + verbose=None, + very_verbose=None, + mutators_to_run=None, + comprehensive=None, + codebase=(TEST_DATA_DIR / "test_source_unit" / "src" / "Counter.sol").as_posix(), + contract_names="Counter", + ), +) +@pytest.mark.skip(reason="Slow test") +def test_mutator(mock_args, solc_binary_path): # pylint: disable=unused-argument + + with change_directory(TEST_DATA_DIR / "test_source_unit"): + main() + + +def test_backup_source_file(solc_binary_path): + solc_path = solc_binary_path("0.8.15") + + file_path = (TEST_DATA_DIR / "test_source_unit" / "src" / "Counter.sol").as_posix() + sl = Slither(file_path, solc=solc_path) + + with tempfile.TemporaryDirectory() as directory: + files_dict = backup_source_file(sl.source_code, Path(directory)) + + assert len(files_dict) == 1 + assert Path(files_dict[file_path]).exists() + + +@pytest.mark.skipif( + not foundry_available or not project_ready, reason="requires Foundry and project setup" +) +def test_get_sol_file_list(): + + project_directory = TEST_DATA_DIR / "test_source_unit" + + files = get_sol_file_list(project_directory, None) + + assert len(files) == 46 + + files = get_sol_file_list(project_directory, ["lib"]) + assert len(files) == 3 + + files = get_sol_file_list(project_directory, ["lib", "script"]) + assert len(files) == 2 + + files = get_sol_file_list(project_directory / "src" / "Counter.sol", None) + assert len(files) == 1 + + (project_directory / "test.sol").mkdir() + files = get_sol_file_list(project_directory, None) + assert all("test.sol" not in file for file in files) + (project_directory / "test.sol").rmdir() + + +@pytest.mark.skipif( + not foundry_available or not project_ready, reason="requires Foundry and project setup" +) +def test_run_test(caplog): + with change_directory(TEST_DATA_DIR / "test_source_unit"): + result = run_test_cmd("forge test", timeout=None, target_file=None, verbose=True) + assert result + assert not caplog.records + + # Failed command + result = run_test_cmd("forge non-test", timeout=None, target_file=None, verbose=True) + assert not result + assert caplog.records + + +def test_run_tests_timeout(caplog, monkeypatch): + def mock_run(*args, **kwargs): + raise subprocess.TimeoutExpired(cmd=args[0], timeout=kwargs.get("timeout")) + + monkeypatch.setattr(subprocess, "run", mock_run) + + with change_directory(TEST_DATA_DIR / "test_source_unit"): + result = run_test_cmd("forge test", timeout=1) + assert not result + assert "Tests took too long" in caplog.messages[0]