From 38cc41a7139ee0c21e3a8dc78c6ce718c7beaa4e Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 18 Oct 2023 14:14:05 -0500 Subject: [PATCH 01/59] Update installation instrucitons --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c4b4457835..37f678d7a1 100644 --- a/README.md +++ b/README.md @@ -73,14 +73,14 @@ If you're **not** going to use one of the [supported compilation frameworks](htt ### Using Pip ```console -pip3 install slither-analyzer +python3 -m pip install slither-analyzer ``` ### Using Git ```bash git clone https://github.com/crytic/slither.git && cd slither -python3 setup.py install +python3 -m pip install . ``` We recommend using a Python virtual environment, as detailed in the [Developer Installation Instructions](https://github.com/trailofbits/slither/wiki/Developer-installation), if you prefer to install Slither via git. From f0f120030842834398f0a5b572bccf734ea6059b Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 18 Oct 2023 14:07:10 -0500 Subject: [PATCH 02/59] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d0a7d67bef..6de5ec2c66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,6 @@ ENV PATH="/home/slither/.local/bin:${PATH}" RUN --mount=type=bind,target=/mnt,source=/wheels,from=python-wheels \ pip3 install --user --no-cache-dir --upgrade --no-index --find-links /mnt --no-deps /mnt/*.whl -RUN solc-select install 0.4.25 && solc-select use 0.4.25 +RUN solc-select use latest --always-install CMD /bin/bash From a9e52aa625ca14a4eb1fbb847b4bc28d89329472 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:03:33 +0000 Subject: [PATCH 03/59] Bump actions/setup-node from 3 to 4 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb8f0ea6e1..1660a25e9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: pip install ".[test]" - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16' cache: 'npm' From c316a814db00a2243c3c321f96f4bd317216c309 Mon Sep 17 00:00:00 2001 From: Usmann Khan Date: Thu, 16 Nov 2023 14:16:26 -0800 Subject: [PATCH 04/59] Error when a missing contract is specified to read-storage. Previously this just silently exited --- slither/tools/read_storage/__main__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/slither/tools/read_storage/__main__.py b/slither/tools/read_storage/__main__.py index 8415ae185f..3baa5d351a 100644 --- a/slither/tools/read_storage/__main__.py +++ b/slither/tools/read_storage/__main__.py @@ -7,6 +7,7 @@ from crytic_compile import cryticparser from slither import Slither +from slither.exceptions import SlitherError from slither.tools.read_storage.read_storage import SlitherReadStorage, RpcInfo @@ -129,6 +130,8 @@ def main() -> None: if args.contract_name: contracts = slither.get_contract_from_name(args.contract_name) + if len(contracts) == 0: + raise SlitherError(f"Contract {args.contract_name} not found.") else: contracts = slither.contracts From 4bfb61ed3d53c5fbb86ebf2c3d0f768b87f5917f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:16 +0000 Subject: [PATCH 05/59] Bump cachix/install-nix-action from 23 to 24 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 23 to 24. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v23...v24) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f04436bd38..4a646bb42d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@v23 + uses: cachix/install-nix-action@v24 - name: Set up cachix if: matrix.type == 'dapp' From 72a8e4b2d6930fd710229f2f9c485a6b91cc41f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:18 +0000 Subject: [PATCH 06/59] Bump actions/configure-pages from 3 to 4 Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 3 to 4. - [Release notes](https://github.com/actions/configure-pages/releases) - [Commits](https://github.com/actions/configure-pages/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/configure-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 29356c0c6b..d92e5b091f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - uses: actions/setup-python@v4 with: python-version: '3.8' From a2c5206d5195596e15feb8d6510f6fea681308b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:22 +0000 Subject: [PATCH 07/59] Bump actions/deploy-pages from 2 to 3 Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 2 to 3. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d92e5b091f..e28c7136a7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,4 +43,4 @@ jobs: path: './html/' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v3 From 6d5def6107aa9a12b44bc073a08e13c0cc8ccecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:26 +0000 Subject: [PATCH 08/59] Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.10 to 1.8.11. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.10...v1.8.11) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7b4d61e89f..e0ab4b5434 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,7 +44,7 @@ jobs: path: dist/ - name: publish - uses: pypa/gh-action-pypi-publish@v1.8.10 + uses: pypa/gh-action-pypi-publish@v1.8.11 - name: sign uses: sigstore/gh-action-sigstore-python@v2.1.0 From 12ac36986c45d072fef02a84376120e21f8137e0 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Fri, 20 Oct 2023 08:52:39 -0500 Subject: [PATCH 09/59] remove unused files --- slither/core/children/__init__.py | 0 slither/core/children/child_event.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 slither/core/children/__init__.py delete mode 100644 slither/core/children/child_event.py diff --git a/slither/core/children/__init__.py b/slither/core/children/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/slither/core/children/child_event.py b/slither/core/children/child_event.py deleted file mode 100644 index e69de29bb2..0000000000 From 435871fc94d5ffc0ce56d5df9ea106d60588ada7 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 1 Nov 2023 12:12:24 -0500 Subject: [PATCH 10/59] fix is_reentrant for internal vyper functions --- slither/core/declarations/function.py | 13 +++++++++---- tests/unit/core/test_function_declaration.py | 9 ++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py index e803154d00..d2baaf7e7b 100644 --- a/slither/core/declarations/function.py +++ b/slither/core/declarations/function.py @@ -1500,10 +1500,13 @@ def is_reentrant(self) -> bool: """ Determine if the function can be re-entered """ + reentrancy_modifier = "nonReentrant" + + if self.function_language == FunctionLanguage.Vyper: + reentrancy_modifier = "nonreentrant(lock)" + # TODO: compare with hash of known nonReentrant modifier instead of the name - if "nonReentrant" in [m.name for m in self.modifiers] or "nonreentrant(lock)" in [ - m.name for m in self.modifiers - ]: + if reentrancy_modifier in [m.name for m in self.modifiers]: return False if self.visibility in ["public", "external"]: @@ -1515,7 +1518,9 @@ def is_reentrant(self) -> bool: ] if not all_entry_points: return True - return not all(("nonReentrant" in [m.name for m in f.modifiers] for f in all_entry_points)) + return not all( + (reentrancy_modifier in [m.name for m in f.modifiers] for f in all_entry_points) + ) # endregion ################################################################################### diff --git a/tests/unit/core/test_function_declaration.py b/tests/unit/core/test_function_declaration.py index cea207613a..f75198d24b 100644 --- a/tests/unit/core/test_function_declaration.py +++ b/tests/unit/core/test_function_declaration.py @@ -324,6 +324,9 @@ def withdraw(): @external @nonreentrant("lock") def withdraw_locked(): + self.withdraw_locked_internal() +@internal +def withdraw_locked_internal(): raw_call(msg.sender, b"", value= self.balances[msg.sender]) @payable @external @@ -376,10 +379,14 @@ def __default__(): assert not f.is_empty f = functions["withdraw_locked()"] - assert not f.is_reentrant + assert f.is_reentrant is False assert f.is_implemented assert not f.is_empty + f = functions["withdraw_locked_internal()"] + assert f.is_reentrant is False + assert f.visibility == "internal" + var = contract.get_state_variable_from_name("balances") assert var assert var.solidity_signature == "balances(address)" From c225727d4d930a1c7875760243a9f2a2d55a4f4b Mon Sep 17 00:00:00 2001 From: Atreay Kukanur <66585295+ATREAY@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:12:48 +0000 Subject: [PATCH 11/59] Substituted the letter 'z' with 'x' in pre-declaration --- slither/detectors/variables/predeclaration_usage_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/detectors/variables/predeclaration_usage_local.py b/slither/detectors/variables/predeclaration_usage_local.py index b4d75e51af..9816dd6e24 100644 --- a/slither/detectors/variables/predeclaration_usage_local.py +++ b/slither/detectors/variables/predeclaration_usage_local.py @@ -36,7 +36,7 @@ class PredeclarationUsageLocal(AbstractDetector): ```solidity contract C { function f(uint z) public returns (uint) { - uint y = x + 9 + z; // 'z' is used pre-declaration + uint y = x + 9 + z; // 'x' is used pre-declaration uint x = 7; if (z % 2 == 0) { From a3dd9b168253e3a74e4bc774d75fc270fb46998e Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Tue, 2 Jan 2024 09:20:59 -0500 Subject: [PATCH 12/59] updated mutator --- slither/tools/mutator/__main__.py | 81 +++++++++++++++---- slither/tools/mutator/mutators/MIA.py | 73 +++++++++++------ slither/tools/mutator/mutators/MVIE.py | 45 ++++++----- slither/tools/mutator/mutators/MVIV.py | 45 ++++++----- .../mutator/mutators/abstract_mutator.py | 19 +++-- slither/tools/mutator/utils/command_line.py | 2 - slither/tools/mutator/utils/file_handling.py | 77 ++++++++++++++++++ .../tools/mutator/utils/generic_patching.py | 30 ++++--- .../tools/mutator/utils/replace_conditions.py | 43 ++++++++++ .../mutator/utils/testing_generated_mutant.py | 31 +++++++ 10 files changed, 349 insertions(+), 97 deletions(-) create mode 100644 slither/tools/mutator/utils/file_handling.py create mode 100644 slither/tools/mutator/utils/replace_conditions.py create mode 100644 slither/tools/mutator/utils/testing_generated_mutant.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 84286ce66c..fde87e5e2c 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -2,7 +2,8 @@ import inspect import logging import sys -from typing import Type, List, Any +from typing import Type, List, Any, Dict, Tuple +import os from crytic_compile import cryticparser @@ -10,9 +11,10 @@ from slither.tools.mutator.mutators import all_mutators from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators +from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list logging.basicConfig() -logger = logging.getLogger("Slither") +logger = logging.getLogger("Slither-Mutate") logger.setLevel(logging.INFO) @@ -22,7 +24,6 @@ ################################################################################### ################################################################################### - def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597", @@ -39,6 +40,22 @@ def parse_args() -> argparse.Namespace: default=False, ) + parser.add_argument( + "--test-cmd", + help="Command line needed to run the tests for your project" + ) + + parser.add_argument( + "--test-dir", + help="Directory of tests" + ) + + # parameter to ignore the interfaces, libraries + parser.add_argument( + "--ignore-dirs", + help="Directories to ignore" + ) + # Initiate all the crytic config cli options cryticparser.init(parser) @@ -73,16 +90,52 @@ def __call__( def main() -> None: - args = parse_args() - - print(args.codebase) - sl = Slither(args.codebase, **vars(args)) - - for compilation_unit in sl.compilation_units: - for M in _get_mutators(): - m = M(compilation_unit) - m.mutate() - - + # print(os.path.isdir(args.codebase)) # provided file/folder + + # arguments + test_command: str = args.test_cmd + test_directory: str = args.test_dir + paths_to_ignore: List[str] = args.ignore_dirs + + # get all the contracts as a list from given codebase + sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore) + + print("Starting Mutation Campaign in", args.codebase, "\n") + for filename in sol_file_list: + # slither object + sl = Slither(filename, **vars(args)) + + # folder where backup files and valid mutants created + output_folder = os.getcwd() + "/mutation_campaign" + + # create a backup files + files_dict = backup_source_file(sl.source_code, output_folder) + + # total count of valid mutants + total_count = 0 + + # mutation + try: + for compilation_unit_of_main_file in sl.compilation_units: + # compilation_unit_of_main_file = sl.compilation_units[-1] + # for i in compilation_unit_of_main_file.contracts: + # print(i.name) + for M in _get_mutators(): + m = M(compilation_unit_of_main_file) + count = m.mutate(test_command, test_directory) + if count != None: + total_count = total_count + count + except Exception as e: + logger.error(e) + + # transfer and delete the backup files + transfer_and_delete(files_dict) + + # output + print(f"Done mutating, '{filename}'") + print(f"Valid mutant count: '{total_count}'\n") + + print("Finished Mutation Campaign in", args.codebase, "\n") # endregion + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 405888f8bf..370f419c18 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,39 +1,60 @@ -from typing import Dict - +from typing import Dict, Tuple from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass - +from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite +from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file_specific_line +from slither.tools.mutator.utils.file_handling import create_mutant_file class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing + VALID_MUTANTS_COUNT = 1 - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: result: Dict = {} - + for contract in self.slither.contracts: - - for function in contract.functions_declared + list(contract.modifiers_declared): - - for node in function.nodes: - if node.type == NodeType.IF: - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] - - # Replace the expression with true - new_str = "true" - - create_patch(result, in_file, start, stop, old_str, new_str) - - return result + if not contract.is_library: + if not contract.is_interface: + for function in contract.functions_declared + list(contract.modifiers_declared): + for node in function.nodes: + if node.contains_if(): + # print(node.expression) + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + # old_str = in_file_str[start:stop] + old_str = str(node.expression) + line_no = node.source_mapping.lines + print(line_no) + # Replace the expression with true + new_str = "true" + + replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) + + # compile and run tests + if compile_generated_mutant(in_file): + if run_test_suite(test_cmd, test_dir): + # print(True) + # generate the mutant and patch + create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) + create_patch(result, in_file, start, stop, old_str, new_str) + + + + return (result, self.VALID_MUTANTS_COUNT) + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index a16a8252e2..9dccc9f131 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,36 +1,41 @@ -from typing import Dict +from typing import Dict, Tuple from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement - +from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing + VALID_MUTANTS_COUNT = 1 - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue - - if not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - return result + if not contract.is_library: + if not contract.is_interface: + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + + return (result, self.VALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index d4a7c54868..9e7b143ae8 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,37 +1,42 @@ -from typing import Dict +from typing import Dict, Tuple from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement - +from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing + VALID_MUTANTS_COUNT = 1 - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue - - if isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - return result + if not contract.is_library: + if not contract.is_interface: + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + return (result, self.VALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 169d8725e4..ab295e2958 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -4,9 +4,10 @@ from typing import Optional, Dict from slither.core.compilation_unit import SlitherCompilationUnit +from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff -logger = logging.getLogger("Slither") +logger = logging.getLogger("Slither-Mutate") class IncorrectMutatorInitialization(Exception): @@ -68,13 +69,14 @@ def __init__( ) @abc.abstractmethod - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" return {} - def mutate(self) -> None: - all_patches = self._mutate() - + def mutate(self, testing_command: str, testing_directory: str) -> int: + # call _mutate function from different mutators + (all_patches, valid_mutant_count) = self._mutate(testing_command, testing_directory) + if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") return @@ -93,4 +95,11 @@ def mutate(self) -> None: diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) if not diff: logger.info(f"Impossible to generate patch; empty {patches}") + # print the differences print(diff) + + return valid_mutant_count + + + + \ No newline at end of file diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index feb479c5c8..80d610a69a 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -1,9 +1,7 @@ from typing import List, Type - from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.utils.myprettytable import MyPrettyTable - def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: mutators_list = [] for detector in mutators_classes: diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py new file mode 100644 index 0000000000..800232a5a5 --- /dev/null +++ b/slither/tools/mutator/utils/file_handling.py @@ -0,0 +1,77 @@ +import os +from typing import Dict, Tuple, List +import logging + +logger = logging.getLogger("Slither-Mutate") + +# function to backup the source file +def backup_source_file(source_code: Dict, output_folder: str) -> Dict: + duplicated_files = {} + os.makedirs(output_folder, exist_ok=True) + + for file_path, content in source_code.items(): + directory, filename = os.path.split(file_path) + new_filename = f"{output_folder}/backup_{filename}" + new_file_path = os.path.join(directory, new_filename) + + with open(new_file_path, 'w') as new_file: + new_file.write(content) + duplicated_files[file_path] = new_file_path + + return duplicated_files + +# function to transfer the original content to the sol file after campaign +def transfer_and_delete(files_dict: Dict) -> None: + try: + for item, value in files_dict.items(): + with open(value, 'r') as duplicated_file: + content = duplicated_file.read() + + with open(item, 'w') as original_file: + original_file.write(content) + + os.remove(value) + except Exception as e: + logger.error(f"Error transferring content: {e}") + +#function to create new mutant file +def create_mutant_file(file: str, count: int, rule: str) -> None: + try: + directory, filename = os.path.split(file) + # Read content from the duplicated file + with open(file, 'r') as source_file: + content = source_file.read() + + # Write content to the original file + mutant_name = filename.split('.')[0] + with open("mutation_campaign/" + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + mutant_file.write(content) + + except Exception as e: + logger.error(f"Error creating mutant: {e}") + +# function to get the contracts list +def get_sol_file_list(codebase: str, ignore_paths: List[str]) -> List[str]: + sol_file_list = [] + + # if input is contract file + if os.path.isfile(codebase): + return [codebase] + + # if input is folder + elif os.path.isdir(codebase): + directory = os.path.abspath(codebase) + for file in os.listdir(directory): + filename = os.path.join(directory, file) + if os.path.isfile(filename): + sol_file_list.append(filename) + elif os.path.isdir(filename): + directory_name, dirname = os.path.split(filename) + if dirname in ignore_paths: + continue + for i in get_sol_file_list(filename, ignore_paths): + sol_file_list.append(i) + + return sol_file_list +# to_do: create a function to delete the commands from the sol file +# def remove_comments(self) -> None: \ No newline at end of file diff --git a/slither/tools/mutator/utils/generic_patching.py b/slither/tools/mutator/utils/generic_patching.py index d773ea7844..03ccadec77 100644 --- a/slither/tools/mutator/utils/generic_patching.py +++ b/slither/tools/mutator/utils/generic_patching.py @@ -1,11 +1,14 @@ from typing import Dict +import os from slither.core.declarations import Contract from slither.core.variables.variable import Variable from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite +from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file +from slither.tools.mutator.utils.file_handling import create_mutant_file - -def remove_assignement(variable: Variable, contract: Contract, result: Dict): +def remove_assignement(variable: Variable, contract: Contract, result: Dict, test_cmd: str, test_dir: str) -> bool: """ Remove the variable's initial assignement @@ -25,12 +28,19 @@ def remove_assignement(variable: Variable, contract: Contract, result: Dict): old_str = in_file_str[start:stop] new_str = old_str[: old_str.find("=")] + + replace_string_in_source_file(in_file, in_file_str[variable.source_mapping.start + old_str.find("="):variable.source_mapping.end], '') - create_patch( - result, - in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - ) + # compile and run tests before the mutant generated before patching + if compile_generated_mutant(in_file): + if run_test_suite(test_cmd, test_dir): + # create_mutant_file(in_file, ) + create_patch( + result, + in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + ) + return True \ No newline at end of file diff --git a/slither/tools/mutator/utils/replace_conditions.py b/slither/tools/mutator/utils/replace_conditions.py new file mode 100644 index 0000000000..82d6c11c98 --- /dev/null +++ b/slither/tools/mutator/utils/replace_conditions.py @@ -0,0 +1,43 @@ +import logging + +logger = logging.getLogger("Slither-Mutate") + +# function to replace the string +def replace_string_in_source_file(file_path: str, old_string: str, new_string: str) -> None: + try: + # Read the content of the Solidity file + with open(file_path, 'r') as file: + content = file.read() + + # Perform the string replacement + modified_content = content.replace(old_string, new_string) + + # Write the modified content back to the file + with open(file_path, 'w') as file: + file.write(modified_content) + + logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.") + except Exception as e: + logger.error(f"Error replacing string: {e}") + +# function to replace the string in a specific line +def replace_string_in_source_file_specific_line(file_path: str, old_string: str, new_string: str, line_number : int) -> None: + try: + # Read the content of the Solidity file + with open(file_path, 'r') as file: + lines = file.readlines() + + if 1 <= line_number <= len(lines): + # Replace the old string with the new string on the specified line + lines[line_number - 1] = lines[line_number - 1].replace(old_string, new_string) + + # Write the modified content back to the file + with open(file_path, 'w') as file: + file.writelines(lines) + + logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.' at '{line_number}") + else: + logger.error(f'Error: Line number {line_number} is out of range') + + except Exception as e: + logger.erro(f'Error: {e}') \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py new file mode 100644 index 0000000000..1c7de9acf8 --- /dev/null +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -0,0 +1,31 @@ +import crytic_compile +import subprocess +import os +import logging + +logger = logging.getLogger("Slither-Mutate") + +# function to compile the generated mutant +def compile_generated_mutant(file_path: str) -> bool: + try: + crytic_compile.CryticCompile(file_path) + return True + except Exception as e: # pylint: disable=broad-except + logger.error("Error Crytic Compile", e) + +# function to run the tests +def run_test_suite(cmd: str, dir: str) -> bool: + try: + # Change to the foundry folder + # os.chdir(dir) + + result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + if not result.stderr: + return True + except subprocess.CalledProcessError as e: + logger.error(f"Error executing 'forge test': {e}") + return False + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return False \ No newline at end of file From 7984ad38af8594a9dec651310b9790959fb73e38 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 3 Jan 2024 11:35:08 -0500 Subject: [PATCH 13/59] Updated replace string logic --- slither/tools/mutator/__main__.py | 31 +++++--- slither/tools/mutator/mutators/MIA.py | 72 ++++++++++--------- slither/tools/mutator/mutators/MVIE.py | 44 ++++++------ slither/tools/mutator/mutators/MVIV.py | 44 ++++++------ .../mutator/mutators/abstract_mutator.py | 8 +-- .../tools/mutator/mutators/all_mutators.py | 4 +- slither/tools/mutator/utils/file_handling.py | 10 ++- .../tools/mutator/utils/replace_conditions.py | 9 ++- .../mutator/utils/testing_generated_mutant.py | 7 +- 9 files changed, 131 insertions(+), 98 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index fde87e5e2c..9cb1a9f8da 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -40,11 +40,13 @@ def parse_args() -> argparse.Namespace: default=False, ) + # argument to add the test command parser.add_argument( "--test-cmd", help="Command line needed to run the tests for your project" ) + # argument to add the test directory - containing all the tests parser.add_argument( "--test-dir", help="Directory of tests" @@ -96,13 +98,14 @@ def main() -> None: # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir - paths_to_ignore: List[str] = args.ignore_dirs + paths_to_ignore: List[str] | None = args.ignore_dirs # get all the contracts as a list from given codebase sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore) print("Starting Mutation Campaign in", args.codebase, "\n") for filename in sol_file_list: + contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) @@ -112,9 +115,12 @@ def main() -> None: # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) - # total count of valid mutants + # total count of mutants total_count = 0 - + + # count of valid mutants + v_count = 0 + # mutation try: for compilation_unit_of_main_file in sl.compilation_units: @@ -123,19 +129,24 @@ def main() -> None: # print(i.name) for M in _get_mutators(): m = M(compilation_unit_of_main_file) - count = m.mutate(test_command, test_directory) - if count != None: - total_count = total_count + count + v_count, i_count = m.mutate(test_command, test_directory, contract_name) + if v_count != None and i_count != None: + total_count = total_count + v_count + i_count except Exception as e: logger.error(e) + except KeyboardInterrupt: + # transfer and delete the backup files if interrupted + logger.error("\nExecution interrupted by user (Ctrl + C). Cleaning up...") + transfer_and_delete(files_dict) + # transfer and delete the backup files transfer_and_delete(files_dict) - - # output - print(f"Done mutating, '{filename}'") - print(f"Valid mutant count: '{total_count}'\n") + + # output + print(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n") + print("Finished Mutation Campaign in", args.codebase, "\n") # endregion \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 370f419c18..b1c55d184f 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -11,47 +11,53 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods HELP = '"if" construct around statement' FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 1 + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: + def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: result: Dict = {} for contract in self.slither.contracts: - if not contract.is_library: - if not contract.is_interface: - for function in contract.functions_declared + list(contract.modifiers_declared): - for node in function.nodes: - if node.contains_if(): - # print(node.expression) - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] + # if not contract.is_library: + # if not contract.is_interface: + if contract_name == str(contract.name): + for function in contract.functions_declared + list(contract.modifiers_declared): + for node in function.nodes: + if node.contains_if(): + # print(node.expression) + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - # old_str = in_file_str[start:stop] - old_str = str(node.expression) - line_no = node.source_mapping.lines - print(line_no) - # Replace the expression with true - new_str = "true" - - replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) - - # compile and run tests - if compile_generated_mutant(in_file): - if run_test_suite(test_cmd, test_dir): - # print(True) - # generate the mutant and patch - create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) - create_patch(result, in_file, start, stop, old_str, new_str) - + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + # old_str = in_file_str[start:stop] + old_str = str(node.expression) + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = "true" + print(line_no[0]) + replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) + + # compile and run tests + if compile_generated_mutant(in_file): + if run_test_suite(test_cmd, test_dir): + # generate the mutant and patch + create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) + create_patch(result, in_file, start, stop, old_str, new_str) + self.VALID_MUTANTS_COUNT = self.VALID_MUTANTS_COUNT + 1 + else: + self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 + else: + self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 + print(self.INVALID_MUTANTS_COUNT) + - return (result, self.VALID_MUTANTS_COUNT) + return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 9dccc9f131..1c99b24eef 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -11,31 +11,33 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods HELP = "variable initialization using an expression" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 1 + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: + def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - if not contract.is_library: - if not contract.is_interface: - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue + # if not contract.is_library: + # if not contract.is_interface: + if contract_name == str(contract.name): + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue - if not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - + if not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + - return (result, self.VALID_MUTANTS_COUNT) + return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 9e7b143ae8..bb1c4cabe0 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -11,32 +11,34 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods HELP = "variable initialization using a value" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 1 + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: + def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - if not contract.is_library: - if not contract.is_interface: - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue + # if not contract.is_library: + # if not contract.is_interface: + if contract_name == str(contract.name): + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue - if isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - + if isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - return (result, self.VALID_MUTANTS_COUNT) + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index ab295e2958..a79f459061 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 enum import Enum -from typing import Optional, Dict +from typing import Optional, Dict, Tuple from slither.core.compilation_unit import SlitherCompilationUnit from slither.tools.doctor.utils import snip_section @@ -73,9 +73,9 @@ def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" return {} - def mutate(self, testing_command: str, testing_directory: str) -> int: + def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[(int, int)]: # call _mutate function from different mutators - (all_patches, valid_mutant_count) = self._mutate(testing_command, testing_directory) + (all_patches, valid_mutant_count, invalid_mutant_count) = self._mutate(testing_command, testing_directory, contract_name) if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") @@ -98,7 +98,7 @@ def mutate(self, testing_command: str, testing_directory: str) -> int: # print the differences print(diff) - return valid_mutant_count + return (valid_mutant_count, invalid_mutant_count) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 5508fb68e5..5a9465c860 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,4 +1,4 @@ # pylint: disable=unused-import -from slither.tools.mutator.mutators.MVIV import MVIV -from slither.tools.mutator.mutators.MVIE import MVIE +# from slither.tools.mutator.mutators.MVIV import MVIV +# from slither.tools.mutator.mutators.MVIE import MVIE from slither.tools.mutator.mutators.MIA import MIA diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index 800232a5a5..c041f76d6e 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -36,6 +36,7 @@ def transfer_and_delete(files_dict: Dict) -> None: #function to create new mutant file def create_mutant_file(file: str, count: int, rule: str) -> None: + try: directory, filename = os.path.split(file) # Read content from the duplicated file @@ -44,16 +45,19 @@ def create_mutant_file(file: str, count: int, rule: str) -> None: # Write content to the original file mutant_name = filename.split('.')[0] - with open("mutation_campaign/" + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + # create folder for each contract + os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) + with open("mutation_campaign/" + mutant_name + '/' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: mutant_file.write(content) except Exception as e: logger.error(f"Error creating mutant: {e}") # function to get the contracts list -def get_sol_file_list(codebase: str, ignore_paths: List[str]) -> List[str]: +def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str]: sol_file_list = [] - + if ignore_paths == None: + ignore_paths = [] # if input is contract file if os.path.isfile(codebase): return [codebase] diff --git a/slither/tools/mutator/utils/replace_conditions.py b/slither/tools/mutator/utils/replace_conditions.py index 82d6c11c98..4e3f91454b 100644 --- a/slither/tools/mutator/utils/replace_conditions.py +++ b/slither/tools/mutator/utils/replace_conditions.py @@ -1,4 +1,5 @@ import logging +import re logger = logging.getLogger("Slither-Mutate") @@ -8,7 +9,7 @@ def replace_string_in_source_file(file_path: str, old_string: str, new_string: s # Read the content of the Solidity file with open(file_path, 'r') as file: content = file.read() - + # Perform the string replacement modified_content = content.replace(old_string, new_string) @@ -28,8 +29,12 @@ def replace_string_in_source_file_specific_line(file_path: str, old_string: str, lines = file.readlines() if 1 <= line_number <= len(lines): + # remove the spaces in the string + line = lines[line_number - 1].replace(" ", "") + old_string = old_string.replace(" ", "") + # Replace the old string with the new string on the specified line - lines[line_number - 1] = lines[line_number - 1].replace(old_string, new_string) + lines[line_number - 1] = line.replace(old_string.strip(), new_string) # Write the modified content back to the file with open(file_path, 'w') as file: diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 1c7de9acf8..61b9db2141 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -20,11 +20,14 @@ def run_test_suite(cmd: str, dir: str) -> bool: # os.chdir(dir) result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - + # result = subprocess.run(cmd.split(' '), check=True) + print(result.stdout) if not result.stderr: return True except subprocess.CalledProcessError as e: - logger.error(f"Error executing 'forge test': {e}") + print(e.output) + logger.error(f"Error executing '{cmd}': {e}") + return False except Exception as e: logger.error(f"An unexpected error occurred: {e}") From 4eba3668e804158df0ee41a95e0fd17494790525 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Fri, 5 Jan 2024 19:31:25 -0500 Subject: [PATCH 14/59] Added new mutant generators --- slither/tools/mutator/__main__.py | 44 ++++++--- slither/tools/mutator/mutators/LOR.py | 47 ++++++++++ slither/tools/mutator/mutators/MIA.py | 65 ++++--------- slither/tools/mutator/mutators/MVIE.py | 48 ++++------ slither/tools/mutator/mutators/MVIV.py | 41 ++++---- slither/tools/mutator/mutators/ROR.py | 53 +++++++++++ slither/tools/mutator/mutators/SBR.py | 94 +++++++++++++++++++ slither/tools/mutator/mutators/UOI.py | 56 +++++++++++ .../mutator/mutators/abstract_mutator.py | 46 ++++++--- .../tools/mutator/mutators/all_mutators.py | 7 +- slither/tools/mutator/utils/file_handling.py | 34 ++++++- .../tools/mutator/utils/generic_patching.py | 28 +++--- .../mutator/utils/testing_generated_mutant.py | 58 +++++++++++- 13 files changed, 467 insertions(+), 154 deletions(-) create mode 100644 slither/tools/mutator/mutators/LOR.py create mode 100644 slither/tools/mutator/mutators/ROR.py create mode 100644 slither/tools/mutator/mutators/SBR.py create mode 100644 slither/tools/mutator/mutators/UOI.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 9cb1a9f8da..201b39acdd 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -4,6 +4,7 @@ import sys from typing import Type, List, Any, Dict, Tuple import os +import shutil from crytic_compile import cryticparser @@ -12,12 +13,12 @@ from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list +from slither.utils.colors import yellow, magenta logging.basicConfig() logger = logging.getLogger("Slither-Mutate") logger.setLevel(logging.INFO) - ################################################################################### ################################################################################### # region Cli Arguments @@ -52,12 +53,17 @@ def parse_args() -> argparse.Namespace: help="Directory of tests" ) - # parameter to ignore the interfaces, libraries + # argument to ignore the interfaces, libraries parser.add_argument( "--ignore-dirs", help="Directories to ignore" ) + # to_do: add time out argument + parser.add_argument( + "--timeout", + help="Set timeout for test command" + ) # Initiate all the crytic config cli options cryticparser.init(parser) @@ -90,7 +96,6 @@ def __call__( ################################################################################### ################################################################################### - def main() -> None: args = parse_args() # print(os.path.isdir(args.codebase)) # provided file/folder @@ -98,19 +103,29 @@ def main() -> None: # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir - paths_to_ignore: List[str] | None = args.ignore_dirs + paths_to_ignore: str | None = args.ignore_dirs + timeout: int = args.timeout + + print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) + + if paths_to_ignore: + paths_to_ignore_list = paths_to_ignore.strip('][').split(',') + print(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) + else: + paths_to_ignore_list = [] # get all the contracts as a list from given codebase - sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore) + sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore_list) + + # folder where backup files and valid mutants created + output_folder = os.getcwd() + "/mutation_campaign" + if os.path.exists(output_folder): + shutil.rmtree(output_folder) - print("Starting Mutation Campaign in", args.codebase, "\n") for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) - - # folder where backup files and valid mutants created - output_folder = os.getcwd() + "/mutation_campaign" # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) @@ -129,9 +144,9 @@ def main() -> None: # print(i.name) for M in _get_mutators(): m = M(compilation_unit_of_main_file) - v_count, i_count = m.mutate(test_command, test_directory, contract_name) - if v_count != None and i_count != None: - total_count = total_count + v_count + i_count + count_valid, count_invalid = m.mutate(test_command, test_directory, contract_name) + v_count += count_valid + total_count += count_valid + count_invalid except Exception as e: logger.error(e) @@ -143,10 +158,9 @@ def main() -> None: # transfer and delete the backup files transfer_and_delete(files_dict) - # output - print(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n") + print(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) - print("Finished Mutation Campaign in", args.codebase, "\n") + print(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) # endregion \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py new file mode 100644 index 0000000000..382a08aeec --- /dev/null +++ b/slither/tools/mutator/mutators/LOR.py @@ -0,0 +1,47 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + +logical_operators = [ + BinaryType.OROR, + BinaryType.ANDAND, +] + +class LOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "LOR" + HELP = "Logical operator replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + + contract = self.contract + + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + for function in contract.functions_and_modifiers_declared: + + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, Binary) and ir.type in logical_operators: + alternative_ops = logical_operators[:] + alternative_ops.remove(ir.type) + + for op in alternative_ops: + + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + # new_str = f"{ir.variable_left} {op.value} {ir.variable_right}" + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index b1c55d184f..d76369d4d4 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,63 +1,38 @@ -from typing import Dict, Tuple +from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite -from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file_specific_line -from slither.tools.mutator.utils.file_handling import create_mutant_file class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 0 - INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: + def _mutate(self) -> Dict: result: Dict = {} + # Retrieve the file + in_file = self.contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = self.contract.compilation_unit.core.source_code[in_file] - for contract in self.slither.contracts: - # if not contract.is_library: - # if not contract.is_interface: - if contract_name == str(contract.name): - for function in contract.functions_declared + list(contract.modifiers_declared): - for node in function.nodes: - if node.contains_if(): - # print(node.expression) - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] + for function in self.contract.functions_declared + list(self.contract.modifiers_declared): + for node in function.nodes: + if node.type == NodeType.IF: + + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - # old_str = in_file_str[start:stop] - old_str = str(node.expression) - line_no = node.source_mapping.lines - # Replace the expression with true - new_str = "true" - print(line_no[0]) - replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) - - # compile and run tests - if compile_generated_mutant(in_file): - if run_test_suite(test_cmd, test_dir): - # generate the mutant and patch - create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) - create_patch(result, in_file, start, stop, old_str, new_str) - self.VALID_MUTANTS_COUNT = self.VALID_MUTANTS_COUNT + 1 - else: - self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 - else: - self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 - print(self.INVALID_MUTANTS_COUNT) + # Replace the expression with true and false + for value in ["true", "false"]: + new_str = value + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) - - - return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + return result diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 1c99b24eef..15b2a20c3a 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,43 +1,35 @@ -from typing import Dict, Tuple +from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement -from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 0 - INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: + def _mutate(self) -> Dict: result: Dict = {} variable: Variable - for contract in self.slither.contracts: - # if not contract.is_library: - # if not contract.is_interface: - if contract_name == str(contract.name): - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue - - if not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - - return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + contract = self.contract + + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if not isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and not isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) + + return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index bb1c4cabe0..5e7c0a6e1b 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -4,41 +4,32 @@ from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement -from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 0 - INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: + def _mutate(self) -> Dict: result: Dict = {} variable: Variable + contract = self.contract + + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue - for contract in self.slither.contracts: - # if not contract.is_library: - # if not contract.is_interface: - if contract_name == str(contract.name): - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue + if isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) - if isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + return result diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py new file mode 100644 index 0000000000..9a8942e196 --- /dev/null +++ b/slither/tools/mutator/mutators/ROR.py @@ -0,0 +1,53 @@ +from typing import Dict +from collections import defaultdict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + + +relational_operators = [ + BinaryType.LESS, + BinaryType.GREATER, + BinaryType.LESS_EQUAL, + BinaryType.GREATER_EQUAL, + BinaryType.EQUAL, + BinaryType.NOT_EQUAL, +] + + +class ROR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "ROR" + HELP = "Relational operator replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + # result["patches"] = defaultdict(list) + contract = self.contract + + for function in contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + # Retrieve the file + in_file = self.contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = self.contract.compilation_unit.core.source_code[in_file] + + if isinstance(ir, Binary) and ir.type in relational_operators: + alternative_ops = relational_operators[:] + alternative_ops.remove(ir.type) + + for op in alternative_ops: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py new file mode 100644 index 0000000000..ac35d9540e --- /dev/null +++ b/slither/tools/mutator/mutators/SBR.py @@ -0,0 +1,94 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +import re + +solidity_rules = [ + "abi\.encode\( ==> abi.encodePacked(", + "abi\.encodePacked\( ==> abi.encode(", + "\.call([({]) ==> .delegatecall\\1", + "\.call([({]) ==> .staticcall\\1", + "\.delegatecall([({]) ==> .call\\1", + "\.delegatecall([({]) ==> .staticcall\\1", + "\.staticcall([({]) ==> .delegatecall\\1", + "\.staticcall([({]) ==> .call\\1", + "^now$ ==> 0", + "block.timestamp ==> 0", + "msg.value ==> 0", + "msg.value ==> 1", + "(\s)(wei|gwei) ==> \\1ether", + "(\s)(ether|gwei) ==> \\1wei", + "(\s)(wei|ether) ==> \\1gwei", + "(\s)(minutes|days|hours|weeks) ==> \\1seconds", + "(\s)(seconds|days|hours|weeks) ==> \\1minutes", + "(\s)(seconds|minutes|hours|weeks) ==> \\1days", + "(\s)(seconds|minutes|days|weeks) ==> \\1hours", + "(\s)(seconds|minutes|days|hours) ==> \\1weeks", + "(\s)(memory) ==> \\1storage", + "(\s)(storage) ==> \\1memory", + "(\s)(constant) ==> \\1immutable", + "addmod ==> mulmod", + "mulmod ==> addmod", + "msg.sender ==> tx.origin", + "tx.origin ==> msg.sender", + "([^u])fixed ==> \\1ufixed", + "ufixed ==> fixed", + "(u?)int16 ==> \\1int8", + "(u?)int32 ==> \\1int16", + "(u?)int64 ==> \\1int32", + "(u?)int128 ==> \\1int64", + "(u?)int256 ==> \\1int128" +] + + +class SBR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "SBR" + HELP = 'Solidity Based Replacements' + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + contract = self.contract + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + + for function in contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type != NodeType.ENTRYPOINT: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + + for variable in contract.state_variables_declared: + node = variable.node_initialization + if node: + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + return result + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/UOI.py b/slither/tools/mutator/mutators/UOI.py new file mode 100644 index 0000000000..6d5862e765 --- /dev/null +++ b/slither/tools/mutator/mutators/UOI.py @@ -0,0 +1,56 @@ +from typing import Dict +import re +from slither.core.expressions.unary_operation import UnaryOperationType +from slither.slithir.variables import Constant +from slither.core.variables.local_variable import LocalVariable +from slither.core.expressions.expression import Expression +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.cfg.node import NodeType + + +unary_operators = [ + UnaryOperationType.PLUSPLUS_PRE, + UnaryOperationType.MINUSMINUS_PRE, + UnaryOperationType.PLUSPLUS_POST, + UnaryOperationType.MINUSMINUS_POST, + UnaryOperationType.MINUS_PRE, +] + + +class UOI(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "UOI" + HELP = "Unary operator insertion" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + + contract = self.contract + + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + + for function in contract.functions_and_modifiers_declared: + for node in function.nodes: + if (node.type == NodeType.EXPRESSION): + for op in unary_operators: + if str(op) in str(node.expression): + for i in node.variables_written: + print(i) + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + # print(old_str) + # Replace the expression with true + # new_str = old_str.replace(str(operand), f"{str(op)}{operand}") + # new_str = re.sub(r'(\w+)\+\+', r'++\1', text) + # print(new_str) + # create_patch(result, in_file, start, stop, old_str, new_str) + print(result) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index a79f459061..e58b705a19 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,12 +1,12 @@ import abc import logging from enum import Enum -from typing import Optional, Dict, Tuple +from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff - +from slither.tools.mutator.utils.testing_generated_mutant import test_patch logger = logging.getLogger("Slither-Mutate") @@ -34,6 +34,8 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- HELP = "" FAULTCLASS = FaultClass.Undefined FAULTNATURE = FaultNature.Undefined + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 def __init__( self, compilation_unit: SlitherCompilationUnit, rate: int = 10, seed: Optional[int] = None @@ -73,13 +75,18 @@ def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" return {} - def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[(int, int)]: + def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[int, int]: + # identify the main contract, ignore the imports + for contract in self.slither.contracts: + if contract_name == str(contract.name): + self.contract = contract + # call _mutate function from different mutators - (all_patches, valid_mutant_count, invalid_mutant_count) = self._mutate(testing_command, testing_directory, contract_name) - + (all_patches) = self._mutate() + if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") - return + return (0,0) for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") @@ -87,18 +94,27 @@ def mutate(self, testing_command: str, testing_directory: str, contract_name: st offset = 0 patches = all_patches["patches"][file] patches.sort(key=lambda x: x["start"]) - if not all(patches[i]["end"] <= patches[i + 1]["end"] for i in range(len(patches) - 1)): - logger.info(f"Impossible to generate patch; patches collisions: {patches}") - continue + # if not all(patches[i]["end"] <= patches[i + 1]["end"] for i in range(len(patches) - 1)): + # logger.error(f"Impossible to generate patch; patches collisions: {patches}") + # continue for patch in patches: - patched_txt, offset = apply_patch(patched_txt, patch, offset) - diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) - if not diff: - logger.info(f"Impossible to generate patch; empty {patches}") + # print(patch) + # test the patch + flag = test_patch(file, patch, testing_command, self.VALID_MUTANTS_COUNT, self.NAME) + # count the valid and invalid mutants + if not flag: + self.INVALID_MUTANTS_COUNT += 1 + continue + self.VALID_MUTANTS_COUNT += 1 + # patched_txt, offset = apply_patch(patched_txt, patch, offset) + # diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) + # if not diff: + # logger.info(f"Impossible to generate patch; empty {patches}") + # print the differences - print(diff) + # print(diff) - return (valid_mutant_count, invalid_mutant_count) + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 5a9465c860..4b0c3b1ada 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,4 +1,9 @@ # pylint: disable=unused-import # from slither.tools.mutator.mutators.MVIV import MVIV # from slither.tools.mutator.mutators.MVIE import MVIE -from slither.tools.mutator.mutators.MIA import MIA +# from slither.tools.mutator.mutators.MIA import MIA +from slither.tools.mutator.mutators.ROR import ROR +# from slither.tools.mutator.mutators.LOR import LOR +# from slither.tools.mutator.mutators.UOI import UOI +# from slither.tools.mutator.mutators.SBR import SBR + diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index c041f76d6e..70f7deb116 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -4,9 +4,10 @@ logger = logging.getLogger("Slither-Mutate") +duplicated_files = {} + # function to backup the source file def backup_source_file(source_code: Dict, output_folder: str) -> Dict: - duplicated_files = {} os.makedirs(output_folder, exist_ok=True) for file_path, content in source_code.items(): @@ -23,7 +24,8 @@ def backup_source_file(source_code: Dict, output_folder: str) -> Dict: # function to transfer the original content to the sol file after campaign def transfer_and_delete(files_dict: Dict) -> None: try: - for item, value in files_dict.items(): + files_dict_copy = files_dict.copy() + for item, value in files_dict_copy.items(): with open(value, 'r') as duplicated_file: content = duplicated_file.read() @@ -31,6 +33,10 @@ def transfer_and_delete(files_dict: Dict) -> None: original_file.write(content) os.remove(value) + + # delete elements from the global dict + del duplicated_files[item] + except Exception as e: logger.error(f"Error transferring content: {e}") @@ -45,14 +51,36 @@ def create_mutant_file(file: str, count: int, rule: str) -> None: # Write content to the original file mutant_name = filename.split('.')[0] + # create folder for each contract os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) - with open("mutation_campaign/" + mutant_name + '/' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: mutant_file.write(content) + # reset the file + with open(duplicated_files[file], 'r') as duplicated_file: + duplicate_content = duplicated_file.read() + + with open(file, 'w') as source_file: + source_file.write(duplicate_content) + except Exception as e: logger.error(f"Error creating mutant: {e}") +# function to reset the file +def reset_file(file: str) -> None: + try: + # directory, filename = os.path.split(file) + # reset the file + with open(duplicated_files[file], 'r') as duplicated_file: + duplicate_content = duplicated_file.read() + + with open(file, 'w') as source_file: + source_file.write(duplicate_content) + + except Exception as e: + logger.error(f"Error resetting file: {e}") + # function to get the contracts list def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str]: sol_file_list = [] diff --git a/slither/tools/mutator/utils/generic_patching.py b/slither/tools/mutator/utils/generic_patching.py index 03ccadec77..6c0ba65117 100644 --- a/slither/tools/mutator/utils/generic_patching.py +++ b/slither/tools/mutator/utils/generic_patching.py @@ -8,7 +8,7 @@ from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file from slither.tools.mutator.utils.file_handling import create_mutant_file -def remove_assignement(variable: Variable, contract: Contract, result: Dict, test_cmd: str, test_dir: str) -> bool: +def remove_assignement(variable: Variable, contract: Contract, result: Dict) -> bool: """ Remove the variable's initial assignement @@ -28,19 +28,13 @@ def remove_assignement(variable: Variable, contract: Contract, result: Dict, tes old_str = in_file_str[start:stop] new_str = old_str[: old_str.find("=")] - - replace_string_in_source_file(in_file, in_file_str[variable.source_mapping.start + old_str.find("="):variable.source_mapping.end], '') - - # compile and run tests before the mutant generated before patching - if compile_generated_mutant(in_file): - if run_test_suite(test_cmd, test_dir): - # create_mutant_file(in_file, ) - create_patch( - result, - in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - ) - return True \ No newline at end of file + line_no = [0] + create_patch( + result, + in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 61b9db2141..7ab4065191 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -2,8 +2,13 @@ import subprocess import os import logging - +import time +import signal +from typing import List, Dict +from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file +from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") +timeout = 30 # we can get it as parameter # function to compile the generated mutant def compile_generated_mutant(file_path: str) -> bool: @@ -11,7 +16,9 @@ def compile_generated_mutant(file_path: str) -> bool: crytic_compile.CryticCompile(file_path) return True except Exception as e: # pylint: disable=broad-except - logger.error("Error Crytic Compile", e) + print(True) + # logger.error("Error Crytic Compile") + return False # function to run the tests def run_test_suite(cmd: str, dir: str) -> bool: @@ -21,14 +28,55 @@ def run_test_suite(cmd: str, dir: str) -> bool: result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # result = subprocess.run(cmd.split(' '), check=True) - print(result.stdout) if not result.stderr: return True except subprocess.CalledProcessError as e: - print(e.output) logger.error(f"Error executing '{cmd}': {e}") return False except Exception as e: logger.error(f"An unexpected error occurred: {e}") - return False \ No newline at end of file + return False + +def run_test_cmd(cmd: str, dir: str) -> bool: + start = time.time() + + # starting new process + P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) + + try: + # checking whether the process is completed or not for 30 seconds + while P.poll() is None and (time.time() - start) < timeout: + time.sleep(0.05) + finally: + if P.poll() is None: + print() + print("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") + # sends a SIGTERM signal to process group - bascially killing the process + os.killpg(os.getpgid(P.pid), signal.SIGTERM) + # Avoid any weird race conditions from grabbing the return code + time.sleep(0.05) + # indicates whether the command executed sucessfully or not + r = P.returncode + + # if r is 0 then it is valid mutant because tests didn't fail + return True if r == 0 else False + +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str) -> bool: + with open(file, 'r') as filepath: + content = filepath.read() + # Perform the replacement based on the index values + replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] + + # Write the modified content back to the file + with open(file, 'w') as filepath: + filepath.write(replaced_content) + if(compile_generated_mutant(file)): + if(run_test_cmd(command, file)): + create_mutant_file(file, index, generator_name) + logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> VALID\n")) + return True + + reset_file(file) + logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> INVALID\n")) + return False \ No newline at end of file From 40305bbf1b4f67af8bbd634898207a15fb129276 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Tue, 9 Jan 2024 12:03:22 -0500 Subject: [PATCH 15/59] Added new mutators --- slither/tools/mutator/__main__.py | 33 ++++++---- slither/tools/mutator/mutators/AOR.py | 38 ++++++++++++ slither/tools/mutator/mutators/ASOR.py | 50 +++++++++++++++ slither/tools/mutator/mutators/BOR.py | 35 +++++++++++ slither/tools/mutator/mutators/LOR.py | 14 +---- slither/tools/mutator/mutators/MIA.py | 20 +++--- slither/tools/mutator/mutators/MVIE.py | 45 +++++++++++--- slither/tools/mutator/mutators/MVIV.py | 45 +++++++++++--- slither/tools/mutator/mutators/MWA.py | 34 +++++++++++ slither/tools/mutator/mutators/ROR.py | 20 ++---- slither/tools/mutator/mutators/SBR.py | 22 +++---- slither/tools/mutator/mutators/UOI.py | 61 +++++++++---------- .../mutator/mutators/abstract_mutator.py | 25 +++++--- .../tools/mutator/mutators/all_mutators.py | 16 +++-- .../tools/mutator/utils/generic_patching.py | 40 ------------ .../tools/mutator/utils/replace_conditions.py | 48 --------------- .../mutator/utils/testing_generated_mutant.py | 13 ++-- 17 files changed, 339 insertions(+), 220 deletions(-) create mode 100644 slither/tools/mutator/mutators/AOR.py create mode 100644 slither/tools/mutator/mutators/ASOR.py create mode 100644 slither/tools/mutator/mutators/BOR.py create mode 100644 slither/tools/mutator/mutators/MWA.py delete mode 100644 slither/tools/mutator/utils/generic_patching.py delete mode 100644 slither/tools/mutator/utils/replace_conditions.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 201b39acdd..aba4273555 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -59,11 +59,18 @@ def parse_args() -> argparse.Namespace: help="Directories to ignore" ) - # to_do: add time out argument + # time out argument parser.add_argument( "--timeout", - help="Set timeout for test command" + help="Set timeout for test command (by deafult 30 seconds)" ) + + # output directory argument + parser.add_argument( + "--output-dir", + help="Output Directory (by default it is 'mutation_campaign')" + ) + # Initiate all the crytic config cli options cryticparser.init(parser) @@ -98,13 +105,13 @@ def __call__( def main() -> None: args = parse_args() - # print(os.path.isdir(args.codebase)) # provided file/folder # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir paths_to_ignore: str | None = args.ignore_dirs - timeout: int = args.timeout + output_dir: str | None = args.output_dir + timeout: int | None = args.timeout print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) @@ -118,33 +125,33 @@ def main() -> None: sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore_list) # folder where backup files and valid mutants created - output_folder = os.getcwd() + "/mutation_campaign" + if output_dir == None: + output_dir = "/mutation_campaign" + output_folder = os.getcwd() + output_dir if os.path.exists(output_folder): shutil.rmtree(output_folder) + # set default timeout + if timeout == None: + timeout = 30 + for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) - # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) - # total count of mutants total_count = 0 - # count of valid mutants v_count = 0 # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - # compilation_unit_of_main_file = sl.compilation_units[-1] - # for i in compilation_unit_of_main_file.contracts: - # print(i.name) for M in _get_mutators(): - m = M(compilation_unit_of_main_file) - count_valid, count_invalid = m.mutate(test_command, test_directory, contract_name) + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name) + count_valid, count_invalid = m.mutate() v_count += count_valid total_count += count_valid + count_invalid except Exception as e: diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py new file mode 100644 index 0000000000..f248d3c771 --- /dev/null +++ b/slither/tools/mutator/mutators/AOR.py @@ -0,0 +1,38 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + +arithmetic_operators = [ + BinaryType.ADDITION, + BinaryType.DIVISION, + BinaryType.MULTIPLICATION, + BinaryType.SUBTRACTION, + BinaryType.MODULO +] + +class AOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "AOR" + HELP = "Arithmetic operator replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, Binary) and ir.type in arithmetic_operators: + alternative_ops = arithmetic_operators[:] + alternative_ops.remove(ir.type) + for op in alternative_ops: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py new file mode 100644 index 0000000000..8c70756a24 --- /dev/null +++ b/slither/tools/mutator/mutators/ASOR.py @@ -0,0 +1,50 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation + +assignment_operators = [ + AssignmentOperationType.ASSIGN_ADDITION, + AssignmentOperationType.ASSIGN_SUBTRACTION, + AssignmentOperationType.ASSIGN, + AssignmentOperationType.ASSIGN_OR, + AssignmentOperationType.ASSIGN_CARET, + AssignmentOperationType.ASSIGN_AND, + AssignmentOperationType.ASSIGN_LEFT_SHIFT, + AssignmentOperationType.ASSIGN_RIGHT_SHIFT, + AssignmentOperationType.ASSIGN_MULTIPLICATION, + AssignmentOperationType.ASSIGN_DIVISION, + AssignmentOperationType.ASSIGN_MODULO +] + +class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "ASOR" + HELP = "Assignment Operator Replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + if isinstance(ir.expression, AssignmentOperation) and ir.expression.type in assignment_operators: + if ir.expression.type == AssignmentOperationType.ASSIGN: + continue + alternative_ops = assignment_operators[:] + try: + alternative_ops.remove(ir.expression.type) + except: + continue + for op in assignment_operators: + if op != ir.expression: + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py new file mode 100644 index 0000000000..7878a20eaa --- /dev/null +++ b/slither/tools/mutator/mutators/BOR.py @@ -0,0 +1,35 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + +bitwise_operators = [ + BinaryType.AND, + BinaryType.OR +] + +class BOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "BOR" + HELP = "Bitwise Operator Replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, Binary) and ir.type in bitwise_operators: + alternative_ops = bitwise_operators[:] + alternative_ops.remove(ir.type) + for op in alternative_ops: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index 382a08aeec..b00b130001 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -15,17 +15,9 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - contract = self.contract - - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - for function in contract.functions_and_modifiers_declared: - + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in logical_operators: @@ -37,11 +29,11 @@ def _mutate(self) -> Dict: # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true # new_str = f"{ir.variable_left} {op.value} {ir.variable_right}" new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index d76369d4d4..1cc0fdb6db 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -2,6 +2,7 @@ from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" @@ -10,32 +11,31 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - # Retrieve the file - in_file = self.contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = self.contract.compilation_unit.core.source_code[in_file] - for function in self.contract.functions_declared + list(self.contract.modifiers_declared): + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type == NodeType.IF: - # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true and false for value in ["true", "false"]: new_str = value - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + # print(node.expression) + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - +# limitations - won't work if it is tenary operation \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 15b2a20c3a..13a4e6d1a6 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,9 +1,8 @@ from typing import Dict - from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.tools.mutator.utils.generic_patching import remove_assignement +from slither.formatters.utils.patches import create_patch class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" @@ -12,24 +11,52 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} variable: Variable - contract = self.contract # Create fault for state variables declaration - for variable in contract.state_variables_declared: + for variable in self.contract.state_variables_declared: if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: continue if not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - for function in contract.functions_declared + list(contract.modifiers_declared): + # Get the string + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] + + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) + + for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: if variable.initialized and not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) + # Get the string + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] + + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 5e7c0a6e1b..e705ff94af 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,9 +1,9 @@ -from typing import Dict, Tuple +from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.tools.mutator.utils.generic_patching import remove_assignement +from slither.formatters.utils.patches import create_patch class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" @@ -12,24 +12,51 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} variable: Variable - contract = self.contract - + # Create fault for state variables declaration - for variable in contract.state_variables_declared: + for variable in self.contract.state_variables_declared: if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: continue if isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) + # Get the string + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] - for function in contract.functions_declared + list(contract.modifiers_declared): + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) + + for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: if variable.initialized and isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] + + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py new file mode 100644 index 0000000000..1e20674251 --- /dev/null +++ b/slither/tools/mutator/mutators/MWA.py @@ -0,0 +1,34 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation + +class MWA(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "MIA" + HELP = '"while" construct around statement' + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type == NodeType.IFLOOP: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result + + + +# limitations - won't work if it is tenary operation + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 9a8942e196..6a2ef6426f 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,5 +1,4 @@ from typing import Dict -from collections import defaultdict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass @@ -22,19 +21,11 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - # result["patches"] = defaultdict(list) - contract = self.contract - for function in contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: - # Retrieve the file - in_file = self.contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = self.contract.compilation_unit.core.source_code[in_file] - if isinstance(ir, Binary) and ir.type in relational_operators: alternative_ops = relational_operators[:] alternative_ops.remove(ir.type) @@ -43,11 +34,12 @@ def _mutate(self) -> Dict: # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result + +# failing in case of condition1 || condition2 \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index ac35d9540e..6797c0c35f 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -3,6 +3,7 @@ from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass import re +from slither.core.variables.variable import Variable solidity_rules = [ "abi\.encode\( ==> abi.encodePacked(", @@ -38,7 +39,8 @@ "(u?)int32 ==> \\1int16", "(u?)int64 ==> \\1int32", "(u?)int128 ==> \\1int64", - "(u?)int256 ==> \\1int128" + "(u?)int256 ==> \\1int128" + "while ==> if", ] @@ -51,40 +53,36 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - contract = self.contract - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] + variable: Variable - for function in contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type != NodeType.ENTRYPOINT: # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - for variable in contract.state_variables_declared: + for variable in self.contract.state_variables_declared: node = variable.node_initialization if node: start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/UOI.py b/slither/tools/mutator/mutators/UOI.py index 6d5862e765..60e8c22c35 100644 --- a/slither/tools/mutator/mutators/UOI.py +++ b/slither/tools/mutator/mutators/UOI.py @@ -1,23 +1,19 @@ from typing import Dict -import re -from slither.core.expressions.unary_operation import UnaryOperationType +from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation +from slither.core.expressions.expression import Expression from slither.slithir.variables import Constant from slither.core.variables.local_variable import LocalVariable -from slither.core.expressions.expression import Expression from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.core.cfg.node import NodeType - unary_operators = [ UnaryOperationType.PLUSPLUS_PRE, UnaryOperationType.MINUSMINUS_PRE, UnaryOperationType.PLUSPLUS_POST, UnaryOperationType.MINUSMINUS_POST, - UnaryOperationType.MINUS_PRE, + UnaryOperationType.MINUS_PRE ] - class UOI(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "UOI" HELP = "Unary operator insertion" @@ -25,32 +21,35 @@ class UOI(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - contract = self.contract - - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - - for function in contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if (node.type == NodeType.EXPRESSION): + try: + ir_expression = node.expression + except Exception as e: + continue + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: for op in unary_operators: - if str(op) in str(node.expression): - for i in node.variables_written: - print(i) - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] - # print(old_str) - # Replace the expression with true - # new_str = old_str.replace(str(operand), f"{str(op)}{operand}") - # new_str = re.sub(r'(\w+)\+\+', r'++\1', text) - # print(new_str) - # create_patch(result, in_file, start, stop, old_str, new_str) - print(result) + if not node.expression.is_prefix: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(variable_read) + str(op) + if new_str != old_str: + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(op) + str(variable_read) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + else: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(op) + str(variable_read) + if new_str != old_str: + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(variable_read) + str(op) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index e58b705a19..ead78ac979 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -38,12 +38,15 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- INVALID_MUTANTS_COUNT = 0 def __init__( - self, compilation_unit: SlitherCompilationUnit, rate: int = 10, seed: Optional[int] = None + self, compilation_unit: SlitherCompilationUnit, timeout: int, testing_command: str, testing_directory: str, contract_name: str, rate: int = 10, seed: Optional[int] = None ): self.compilation_unit = compilation_unit self.slither = compilation_unit.core self.seed = seed self.rate = rate + self.test_command = testing_command + self.test_directory = testing_directory + self.timeout = timeout if not self.NAME: raise IncorrectMutatorInitialization( @@ -69,18 +72,23 @@ def __init__( raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" ) + + # identify the main contract, ignore the imports + for contract in self.slither.contracts: + if contract_name == str(contract.name): # limitation: what if the contract name is not same as file name + # contract + self.contract = contract + # Retrieve the file + self.in_file = self.contract.source_mapping.filename.absolute + # Retrieve the source code + self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] @abc.abstractmethod def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" return {} - def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[int, int]: - # identify the main contract, ignore the imports - for contract in self.slither.contracts: - if contract_name == str(contract.name): - self.contract = contract - + def mutate(self) -> Tuple[int, int]: # call _mutate function from different mutators (all_patches) = self._mutate() @@ -98,9 +106,8 @@ def mutate(self, testing_command: str, testing_directory: str, contract_name: st # logger.error(f"Impossible to generate patch; patches collisions: {patches}") # continue for patch in patches: - # print(patch) # test the patch - flag = test_patch(file, patch, testing_command, self.VALID_MUTANTS_COUNT, self.NAME) + flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 4b0c3b1ada..3f4e590436 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,9 +1,13 @@ # pylint: disable=unused-import -# from slither.tools.mutator.mutators.MVIV import MVIV -# from slither.tools.mutator.mutators.MVIE import MVIE -# from slither.tools.mutator.mutators.MIA import MIA +from slither.tools.mutator.mutators.MVIV import MVIV +from slither.tools.mutator.mutators.MVIE import MVIE +from slither.tools.mutator.mutators.MIA import MIA from slither.tools.mutator.mutators.ROR import ROR -# from slither.tools.mutator.mutators.LOR import LOR -# from slither.tools.mutator.mutators.UOI import UOI -# from slither.tools.mutator.mutators.SBR import SBR +from slither.tools.mutator.mutators.LOR import LOR +from slither.tools.mutator.mutators.UOI import UOI +from slither.tools.mutator.mutators.SBR import SBR +from slither.tools.mutator.mutators.AOR import AOR +from slither.tools.mutator.mutators.BOR import BOR +from slither.tools.mutator.mutators.ASOR import ASOR +from slither.tools.mutator.mutators.MWA import MWA diff --git a/slither/tools/mutator/utils/generic_patching.py b/slither/tools/mutator/utils/generic_patching.py deleted file mode 100644 index 6c0ba65117..0000000000 --- a/slither/tools/mutator/utils/generic_patching.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Dict -import os - -from slither.core.declarations import Contract -from slither.core.variables.variable import Variable -from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite -from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file -from slither.tools.mutator.utils.file_handling import create_mutant_file - -def remove_assignement(variable: Variable, contract: Contract, result: Dict) -> bool: - """ - Remove the variable's initial assignement - - :param variable: - :param contract: - :param result: - :return: - """ - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - - # Get the string - start = variable.source_mapping.start - stop = variable.expression.source_mapping.start - old_str = in_file_str[start:stop] - - new_str = old_str[: old_str.find("=")] - line_no = [0] - create_patch( - result, - in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no - ) \ No newline at end of file diff --git a/slither/tools/mutator/utils/replace_conditions.py b/slither/tools/mutator/utils/replace_conditions.py deleted file mode 100644 index 4e3f91454b..0000000000 --- a/slither/tools/mutator/utils/replace_conditions.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -import re - -logger = logging.getLogger("Slither-Mutate") - -# function to replace the string -def replace_string_in_source_file(file_path: str, old_string: str, new_string: str) -> None: - try: - # Read the content of the Solidity file - with open(file_path, 'r') as file: - content = file.read() - - # Perform the string replacement - modified_content = content.replace(old_string, new_string) - - # Write the modified content back to the file - with open(file_path, 'w') as file: - file.write(modified_content) - - logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.") - except Exception as e: - logger.error(f"Error replacing string: {e}") - -# function to replace the string in a specific line -def replace_string_in_source_file_specific_line(file_path: str, old_string: str, new_string: str, line_number : int) -> None: - try: - # Read the content of the Solidity file - with open(file_path, 'r') as file: - lines = file.readlines() - - if 1 <= line_number <= len(lines): - # remove the spaces in the string - line = lines[line_number - 1].replace(" ", "") - old_string = old_string.replace(" ", "") - - # Replace the old string with the new string on the specified line - lines[line_number - 1] = line.replace(old_string.strip(), new_string) - - # Write the modified content back to the file - with open(file_path, 'w') as file: - file.writelines(lines) - - logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.' at '{line_number}") - else: - logger.error(f'Error: Line number {line_number} is out of range') - - except Exception as e: - logger.erro(f'Error: {e}') \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 7ab4065191..61a688f7c5 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -38,14 +38,12 @@ def run_test_suite(cmd: str, dir: str) -> bool: logger.error(f"An unexpected error occurred: {e}") return False -def run_test_cmd(cmd: str, dir: str) -> bool: +def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: start = time.time() - # starting new process P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) - try: - # checking whether the process is completed or not for 30 seconds + # checking whether the process is completed or not within 30 seconds(default) while P.poll() is None and (time.time() - start) < timeout: time.sleep(0.05) finally: @@ -58,21 +56,20 @@ def run_test_cmd(cmd: str, dir: str) -> bool: time.sleep(0.05) # indicates whether the command executed sucessfully or not r = P.returncode - + # if r is 0 then it is valid mutant because tests didn't fail return True if r == 0 else False -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str) -> bool: +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int) -> bool: with open(file, 'r') as filepath: content = filepath.read() # Perform the replacement based on the index values replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] - # Write the modified content back to the file with open(file, 'w') as filepath: filepath.write(replaced_content) if(compile_generated_mutant(file)): - if(run_test_cmd(command, file)): + if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> VALID\n")) return True From 107c42bfdfe1bae5971774beb5853a416a2845d3 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Sun, 14 Jan 2024 19:19:58 -0500 Subject: [PATCH 16/59] Updated mutators --- slither/tools/mutator/__main__.py | 48 +++++++---- slither/tools/mutator/mutators/AOR.py | 10 ++- slither/tools/mutator/mutators/ASOR.py | 6 +- slither/tools/mutator/mutators/BOR.py | 8 +- slither/tools/mutator/mutators/FHR.py | 36 ++++++++ slither/tools/mutator/mutators/LIR.py | 82 ++++++++++++++++++ slither/tools/mutator/mutators/LOR.py | 7 +- slither/tools/mutator/mutators/MIA.py | 18 ++-- slither/tools/mutator/mutators/MVIE.py | 11 ++- slither/tools/mutator/mutators/MVIV.py | 13 ++- slither/tools/mutator/mutators/MWA.py | 13 +-- slither/tools/mutator/mutators/RCR.py | 36 ++++++++ slither/tools/mutator/mutators/ROR.py | 34 ++++---- slither/tools/mutator/mutators/SBR.py | 9 +- .../tools/mutator/mutators/{UOI.py => UOR.py} | 18 ++-- .../mutator/mutators/abstract_mutator.py | 86 +++++++++++-------- .../tools/mutator/mutators/all_mutators.py | 26 +++--- slither/tools/mutator/utils/command_line.py | 11 ++- slither/tools/mutator/utils/file_handling.py | 4 +- .../mutator/utils/testing_generated_mutant.py | 58 +++++++------ 20 files changed, 351 insertions(+), 183 deletions(-) create mode 100644 slither/tools/mutator/mutators/FHR.py create mode 100644 slither/tools/mutator/mutators/LIR.py create mode 100644 slither/tools/mutator/mutators/RCR.py rename slither/tools/mutator/mutators/{UOI.py => UOR.py} (81%) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index aba4273555..c41a82e35f 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -2,12 +2,10 @@ import inspect import logging import sys -from typing import Type, List, Any, Dict, Tuple import os import shutil - +from typing import Type, List, Any from crytic_compile import cryticparser - from slither import Slither from slither.tools.mutator.mutators import all_mutators from .mutators.abstract_mutator import AbstractMutator @@ -62,13 +60,27 @@ def parse_args() -> argparse.Namespace: # time out argument parser.add_argument( "--timeout", - help="Set timeout for test command (by deafult 30 seconds)" + help="Set timeout for test command (by default 30 seconds)" ) # output directory argument parser.add_argument( "--output-dir", - help="Output Directory (by default it is 'mutation_campaign')" + help="Output Directory (by default 'mutation_campaign')" + ) + + # to print just all the mutants + parser.add_argument( + "--verbose", + help="output all mutants generated", + action="store_true", + default=False, + ) + + # select list of mutators to run + parser.add_argument( + "--mutators-to-run", + help="mutant generators to run", ) # Initiate all the crytic config cli options @@ -80,13 +92,14 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() - -def _get_mutators() -> List[Type[AbstractMutator]]: +def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] - detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator)] + if not mutators_list is None: + detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) and str(c.NAME) in mutators_list ] + else: + detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) ] return detectors - class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def __call__( self, parser: Any, *args: Any, **kwargs: Any @@ -105,13 +118,15 @@ def __call__( def main() -> None: args = parse_args() - # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir paths_to_ignore: str | None = args.ignore_dirs output_dir: str | None = args.output_dir timeout: int | None = args.timeout + solc_remappings: str | None = args.solc_remaps + verbose: bool = args.verbose + mutators_to_run: List[str] | None = args.mutators_to_run print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) @@ -137,6 +152,7 @@ def main() -> None: for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] + # TODO: user provides contract name # slither object sl = Slither(filename, **vars(args)) # create a backup files @@ -149,11 +165,13 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - for M in _get_mutators(): - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name) - count_valid, count_invalid = m.mutate() - v_count += count_valid - total_count += count_valid + count_invalid + for M in _get_mutators(mutators_to_run): + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) + # check whether the contract instance exists or not + if m.get_exist_flag(): + count_valid, count_invalid = m.mutate() + v_count += count_valid + total_count += count_valid + count_invalid except Exception as e: logger.error(e) diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index f248d3c771..39e3a6a20e 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -1,7 +1,8 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.core.expressions.unary_operation import UnaryOperation arithmetic_operators = [ BinaryType.ADDITION, @@ -14,7 +15,6 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "AOR" HELP = "Arithmetic operator replacement" - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -22,8 +22,14 @@ def _mutate(self) -> Dict: for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: + try: + ir_expression = node.expression + except: + continue for ir in node.irs: if isinstance(ir, Binary) and ir.type in arithmetic_operators: + if isinstance(ir_expression, UnaryOperation): + continue alternative_ops = arithmetic_operators[:] alternative_ops.remove(ir.type) for op in alternative_ops: diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 8c70756a24..04ad8f8b03 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,7 +1,6 @@ from typing import Dict -from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation assignment_operators = [ @@ -21,7 +20,6 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ASOR" HELP = "Assignment Operator Replacement" - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -38,7 +36,7 @@ def _mutate(self) -> Dict: alternative_ops.remove(ir.expression.type) except: continue - for op in assignment_operators: + for op in alternative_ops: if op != ir.expression: start = node.source_mapping.start stop = start + node.source_mapping.length diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 7878a20eaa..de9ad287e1 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -1,17 +1,19 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature bitwise_operators = [ BinaryType.AND, - BinaryType.OR + BinaryType.OR, + BinaryType.LEFT_SHIFT, + BinaryType.RIGHT_SHIFT, + BinaryType.CARET ] class BOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "BOR" HELP = "Bitwise Operator Replacement" - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py new file mode 100644 index 0000000000..5709346b15 --- /dev/null +++ b/slither/tools/mutator/mutators/FHR.py @@ -0,0 +1,36 @@ +from typing import Dict +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +import re + +# INFO: low severity + +function_header_replacements = [ + "pure ==> view", + "view ==> pure", + "(\s)(external|public|internal) ==> \\1private", + "(\s)(external|public) ==> \\1internal" +] + +class FHR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "FHR" + HELP = 'Function Header Replacement' + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + # function_header = function.source_mapping.content.split('{')[0] + start = function.source_mapping.start + stop = start + function.source_mapping.content.find('{') + old_str = self.in_file_str[start:stop] + line_no = function.source_mapping.lines + for value in function_header_replacements: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py new file mode 100644 index 0000000000..99228913aa --- /dev/null +++ b/slither/tools/mutator/mutators/LIR.py @@ -0,0 +1,82 @@ +from typing import Dict +from slither.core.expressions import Literal +from slither.core.variables.variable import Variable +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.formatters.utils.patches import create_patch +from slither.core.solidity_types import ElementaryType + +literal_replacements = [] + +class LIR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "LIR" + HELP = "Literal Interger Replacement" + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + variable: Variable + + # Create fault for state variables declaration + for variable in self.contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if isinstance(variable.expression, Literal): + if isinstance(variable.type, ElementaryType): + literal_replacements.append(variable.type.min) # append data type min value + 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"): + literal_replacements.append('-1') + # Get the string + start = variable.source_mapping.start + stop = start + variable.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = variable.node_initialization.source_mapping.lines + # line_no = [0] + for value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != value: + new_str = f"{old_str.split('=')[0]}= {value}" + create_patch( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) + + for function in self.contract.functions_and_modifiers_declared: + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + if isinstance(variable.type, ElementaryType): + literal_replacements.append(variable.type.min) + literal_replacements.append(variable.type.max) + if str(variable.type).startswith("uint"): + literal_replacements.append('1') + elif str(variable.type).startswith("uint"): + literal_replacements.append('-1') + start = variable.source_mapping.start + stop = start + variable.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = variable.source_mapping.lines + for new_value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != new_value: + new_str = f"{old_str.split('=')[0]}= {new_value}" + create_patch( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) + + return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index b00b130001..fa66c7bca2 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature logical_operators = [ BinaryType.OROR, @@ -10,8 +10,7 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LOR" - HELP = "Logical operator replacement" - FAULTCLASS = FaultClass.Checking + HELP = "Logical Operator Replacement" FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -25,14 +24,12 @@ def _mutate(self) -> Dict: alternative_ops.remove(ir.type) for op in alternative_ops: - # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true - # new_str = f"{ir.variable_left} {op.value} {ir.variable_right}" new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 1cc0fdb6db..f5bc52b16e 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -17,8 +16,8 @@ def _mutate(self) -> Dict: for node in function.nodes: if node.type == NodeType.IF: # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length + start = node.expression.source_mapping.start + stop = start + node.expression.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines @@ -27,15 +26,10 @@ def _mutate(self) -> Dict: new_str = value create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - # print(node.expression) if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result - - -# limitations - won't work if it is tenary operation - - \ No newline at end of file + print(node.expression) + + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 13a4e6d1a6..3c334e9e68 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.formatters.utils.patches import create_patch class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" - FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -28,7 +27,7 @@ def _mutate(self) -> Dict: old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.node_initialization.source_mapping.lines create_patch( result, self.in_file, @@ -36,7 +35,7 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) for function in self.contract.functions_and_modifiers_declared: @@ -48,7 +47,7 @@ def _mutate(self) -> Dict: old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.source_mapping.lines create_patch( result, self.in_file, @@ -56,7 +55,7 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index e705ff94af..7f75b1dcf1 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -2,13 +2,12 @@ from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.formatters.utils.patches import create_patch class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" - FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -27,9 +26,8 @@ def _mutate(self) -> Dict: start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.node_initialization.source_mapping.lines create_patch( result, self.in_file, @@ -37,7 +35,7 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) for function in self.contract.functions_and_modifiers_declared: @@ -46,9 +44,8 @@ def _mutate(self) -> Dict: start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.source_mapping.lines create_patch( result, self.in_file, @@ -56,7 +53,7 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 1e20674251..c6bacf485d 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MWA(AbstractMutator): # pylint: disable=too-few-public-methods - NAME = "MIA" + NAME = "MWA" HELP = '"while" construct around statement' - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -25,10 +24,4 @@ def _mutate(self) -> Dict: if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result - - - -# limitations - won't work if it is tenary operation - - \ No newline at end of file + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/RCR.py b/slither/tools/mutator/mutators/RCR.py new file mode 100644 index 0000000000..96aebc68f9 --- /dev/null +++ b/slither/tools/mutator/mutators/RCR.py @@ -0,0 +1,36 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature + + +class RCR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "RCR" + HELP = 'Revert and Comment Replacement' + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + if old_str != 'revert()': + new_str = 'revert()' + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + new_str = "//" + old_str + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + return result + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 6a2ef6426f..e1c78c1eb0 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,8 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass - +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature relational_operators = [ BinaryType.LESS, @@ -13,11 +12,9 @@ BinaryType.NOT_EQUAL, ] - class ROR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ROR" - HELP = "Relational operator replacement" - FAULTCLASS = FaultClass.Checking + HELP = "Relational Operator Replacement" FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -27,19 +24,18 @@ def _mutate(self) -> Dict: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in relational_operators: - alternative_ops = relational_operators[:] - alternative_ops.remove(ir.type) - - for op in alternative_ops: - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] - line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if str(ir.variable_left.type) != 'address' and str(ir.variable_right) != 'address': + alternative_ops = relational_operators[:] + alternative_ops.remove(ir.type) + for op in alternative_ops: + # Get the string + start = ir.expression.source_mapping.start + stop = start + ir.expression.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result -# failing in case of condition1 || condition2 \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 6797c0c35f..91c05b884e 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re from slither.core.variables.variable import Variable @@ -39,15 +39,14 @@ "(u?)int32 ==> \\1int16", "(u?)int64 ==> \\1int32", "(u?)int128 ==> \\1int64", - "(u?)int256 ==> \\1int128" - "while ==> if", + "(u?)int256 ==> \\1int128", + "while ==> if", ] class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" - HELP = 'Solidity Based Replacements' - FAULTCLASS = FaultClass.Checking + HELP = 'Solidity Based Replacement' FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/mutators/UOI.py b/slither/tools/mutator/mutators/UOR.py similarity index 81% rename from slither/tools/mutator/mutators/UOI.py rename to slither/tools/mutator/mutators/UOR.py index 60e8c22c35..671ee676d5 100644 --- a/slither/tools/mutator/mutators/UOI.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -1,10 +1,7 @@ from typing import Dict from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation -from slither.core.expressions.expression import Expression -from slither.slithir.variables import Constant -from slither.core.variables.local_variable import LocalVariable from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature unary_operators = [ UnaryOperationType.PLUSPLUS_PRE, @@ -14,10 +11,9 @@ UnaryOperationType.MINUS_PRE ] -class UOI(AbstractMutator): # pylint: disable=too-few-public-methods - NAME = "UOI" - HELP = "Unary operator insertion" - FAULTCLASS = FaultClass.Checking +class UOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "UOR" + HELP = "Unary Operator Replacement" FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -27,7 +23,7 @@ def _mutate(self) -> Dict: for node in function.nodes: try: ir_expression = node.expression - except Exception as e: + except: continue start = node.source_mapping.start stop = start + node.source_mapping.length @@ -39,7 +35,7 @@ def _mutate(self) -> Dict: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(variable_read) + str(op) - if new_str != old_str: + if new_str != old_str and str(op) != '-': create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(op) + str(variable_read) create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) @@ -47,7 +43,7 @@ def _mutate(self) -> Dict: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(op) + str(variable_read) - if new_str != old_str: + if new_str != old_str and str(op) != '-': create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(variable_read) + str(op) create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index ead78ac979..5223e2d81f 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,45 +1,48 @@ import abc import logging from enum import Enum -from typing import Optional, Dict, Tuple, List - +from typing import Optional, Dict, Tuple from slither.core.compilation_unit import SlitherCompilationUnit -from slither.tools.doctor.utils import snip_section +# from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff from slither.tools.mutator.utils.testing_generated_mutant import test_patch -logger = logging.getLogger("Slither-Mutate") +from slither.utils.colors import yellow +logger = logging.getLogger("Slither-Mutate") class IncorrectMutatorInitialization(Exception): pass - -class FaultClass(Enum): - Assignement = 0 - Checking = 1 - Interface = 2 - Algorithm = 3 - Undefined = 100 - - class FaultNature(Enum): Missing = 0 Wrong = 1 Extraneous = 2 Undefined = 100 - + # not executed - can be detected by replacing with revert + # has no effect - can be detected by removing a line / comment + # can have valid mutant + # can't have valid mutant + class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods NAME = "" HELP = "" - FAULTCLASS = FaultClass.Undefined FAULTNATURE = FaultNature.Undefined VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 def __init__( - self, compilation_unit: SlitherCompilationUnit, timeout: int, testing_command: str, testing_directory: str, contract_name: str, rate: int = 10, seed: Optional[int] = None - ): + self, compilation_unit: SlitherCompilationUnit, + timeout: int, + testing_command: str, + testing_directory: str, + contract_name: str, + solc_remappings: str | None, + verbose: bool, + output_folder: str, + rate: int = 10, + seed: Optional[int] = None + ) -> None: self.compilation_unit = compilation_unit self.slither = compilation_unit.core self.seed = seed @@ -47,6 +50,10 @@ def __init__( self.test_command = testing_command self.test_directory = testing_directory self.timeout = timeout + self.contract_exist = False + self.solc_remappings = solc_remappings + self.verbose = verbose + self.output_folder = output_folder if not self.NAME: raise IncorrectMutatorInitialization( @@ -58,11 +65,6 @@ def __init__( f"HELP is not initialized {self.__class__.__name__}" ) - if self.FAULTCLASS == FaultClass.Undefined: - raise IncorrectMutatorInitialization( - f"FAULTCLASS is not initialized {self.__class__.__name__}" - ) - if self.FAULTNATURE == FaultNature.Undefined: raise IncorrectMutatorInitialization( f"FAULTNATURE is not initialized {self.__class__.__name__}" @@ -75,14 +77,25 @@ def __init__( # identify the main contract, ignore the imports for contract in self.slither.contracts: - if contract_name == str(contract.name): # limitation: what if the contract name is not same as file name + # !limitation: what if the contract name is not same as file name + # !limitation: multi contract + if contract_name.lower() == str(contract.name).lower(): # contract self.contract = contract # Retrieve the file self.in_file = self.contract.source_mapping.filename.absolute # Retrieve the source code self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] - + # flag contract existence + self.contract_exist = True + + if not self.contract_exist: + self.contract_exist = False + logger.error(f"Contract name is not matching with the File name ({contract_name}). Please refer 'https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names')") + + def get_exist_flag(self) -> bool: + return self.contract_exist + @abc.abstractmethod def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" @@ -95,31 +108,28 @@ def mutate(self) -> Tuple[int, int]: if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") return (0,0) - + for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") - patched_txt = original_txt - offset = 0 patches = all_patches["patches"][file] patches.sort(key=lambda x: x["start"]) - # if not all(patches[i]["end"] <= patches[i + 1]["end"] for i in range(len(patches) - 1)): - # logger.error(f"Impossible to generate patch; patches collisions: {patches}") - # continue + print(yellow(f"Mutating {file} with {self.NAME} \n")) for patch in patches: # test the patch - flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout) + flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 continue self.VALID_MUTANTS_COUNT += 1 - # patched_txt, offset = apply_patch(patched_txt, patch, offset) - # diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) - # if not diff: - # logger.info(f"Impossible to generate patch; empty {patches}") - - # print the differences - # print(diff) + patched_txt,_ = apply_patch(original_txt, patch, 0) + diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) + if not diff: + logger.info(f"Impossible to generate patch; empty {patches}") + + # add valid mutant patches to a output file + with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: + patches_file.write(diff + '\n') return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 3f4e590436..9aa81a0fc4 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,13 +1,17 @@ # pylint: disable=unused-import -from slither.tools.mutator.mutators.MVIV import MVIV -from slither.tools.mutator.mutators.MVIE import MVIE -from slither.tools.mutator.mutators.MIA import MIA -from slither.tools.mutator.mutators.ROR import ROR -from slither.tools.mutator.mutators.LOR import LOR -from slither.tools.mutator.mutators.UOI import UOI -from slither.tools.mutator.mutators.SBR import SBR -from slither.tools.mutator.mutators.AOR import AOR -from slither.tools.mutator.mutators.BOR import BOR -from slither.tools.mutator.mutators.ASOR import ASOR -from slither.tools.mutator.mutators.MWA import MWA +from slither.tools.mutator.mutators.MVIV import MVIV # severity low +from slither.tools.mutator.mutators.MVIE import MVIE # severity low +from slither.tools.mutator.mutators.LOR import LOR # severity medium +from slither.tools.mutator.mutators.UOR import UOR # severity medium +from slither.tools.mutator.mutators.SBR import SBR # severity medium +from slither.tools.mutator.mutators.AOR import AOR # severity medium +from slither.tools.mutator.mutators.BOR import BOR # severity medium +from slither.tools.mutator.mutators.ASOR import ASOR # severity medium +from slither.tools.mutator.mutators.MWA import MWA # severity medium +from slither.tools.mutator.mutators.LIR import LIR # severity medium +from slither.tools.mutator.mutators.FHR import FHR # severity medium +from slither.tools.mutator.mutators.MIA import MIA # severity medium +from slither.tools.mutator.mutators.ROR import ROR # severity medium +from slither.tools.mutator.mutators.RCR import RCR # severity high + diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index 80d610a69a..042b4fff70 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -7,15 +7,14 @@ def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: for detector in mutators_classes: argument = detector.NAME help_info = detector.HELP - fault_class = detector.FAULTCLASS.name fault_nature = detector.FAULTNATURE.name - mutators_list.append((argument, help_info, fault_class, fault_nature)) - table = MyPrettyTable(["Num", "Name", "What it Does", "Fault Class", "Fault Nature"]) + mutators_list.append((argument, help_info, fault_nature)) + table = MyPrettyTable(["Num", "Name", "What it Does", "Fault Nature"]) # Sort by class, nature, name - mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[3], element[0])) + mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[0])) idx = 1 - for (argument, help_info, fault_class, fault_nature) in mutators_list: - table.add_row([str(idx), argument, help_info, fault_class, fault_nature]) + for (argument, help_info, fault_nature) in mutators_list: + table.add_row([str(idx), argument, help_info, fault_nature]) idx = idx + 1 print(table) diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index 70f7deb116..d61bba58af 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -105,5 +105,5 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str sol_file_list.append(i) return sol_file_list -# to_do: create a function to delete the commands from the sol file -# def remove_comments(self) -> None: \ No newline at end of file + +# TODO: create a function to delete the commands from the sol file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 61a688f7c5..6c6e80a556 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -7,38 +7,44 @@ from typing import List, Dict from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file from slither.utils.colors import green, red + logger = logging.getLogger("Slither-Mutate") -timeout = 30 # we can get it as parameter # function to compile the generated mutant -def compile_generated_mutant(file_path: str) -> bool: +def compile_generated_mutant(file_path: str, mappings: str) -> bool: try: - crytic_compile.CryticCompile(file_path) + crytic_compile.CryticCompile(file_path, solc_remaps=mappings) return True - except Exception as e: # pylint: disable=broad-except - print(True) + except: # pylint: disable=broad-except # logger.error("Error Crytic Compile") return False # function to run the tests -def run_test_suite(cmd: str, dir: str) -> bool: - try: - # Change to the foundry folder - # os.chdir(dir) +# def run_test_suite(cmd: str, dir: str) -> bool: +# try: +# # Change to the foundry folder +# # os.chdir(dir) - result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # result = subprocess.run(cmd.split(' '), check=True) - if not result.stderr: - return True - except subprocess.CalledProcessError as e: - logger.error(f"Error executing '{cmd}': {e}") +# result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +# # result = subprocess.run(cmd.split(' '), check=True) +# if not result.stderr: +# return True +# except subprocess.CalledProcessError as e: +# logger.error(f"Error executing '{cmd}': {e}") - return False - except Exception as e: - logger.error(f"An unexpected error occurred: {e}") - return False +# return False +# except Exception as e: +# logger.error(f"An unexpected error occurred: {e}") +# return False def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: + # add --fail-fast for foundry tests, to exit after first failure + if "forge test" in cmd and not "--fail-fast" in cmd : + cmd += " --fail-fast" + # add --bail for hardhat and truffle tests, to exit after first failure + elif not "--bail" in cmd: + cmd += " --bail" + start = time.time() # starting new process P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) @@ -48,19 +54,18 @@ def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: time.sleep(0.05) finally: if P.poll() is None: - print() - print("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") + logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") # sends a SIGTERM signal to process group - bascially killing the process os.killpg(os.getpgid(P.pid), signal.SIGTERM) # Avoid any weird race conditions from grabbing the return code time.sleep(0.05) # indicates whether the command executed sucessfully or not r = P.returncode - + # if r is 0 then it is valid mutant because tests didn't fail return True if r == 0 else False -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int) -> bool: +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: with open(file, 'r') as filepath: content = filepath.read() # Perform the replacement based on the index values @@ -68,12 +73,13 @@ def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: # Write the modified content back to the file with open(file, 'w') as filepath: filepath.write(replaced_content) - if(compile_generated_mutant(file)): + if(compile_generated_mutant(file, mappings)): if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) - logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> VALID\n")) + logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) return True reset_file(file) - logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> INVALID\n")) + if verbose: + logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) return False \ No newline at end of file From f001bade851a46748647bbd1d5779287be754794 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Tue, 16 Jan 2024 12:00:08 -0500 Subject: [PATCH 17/59] Updated run_test_cmd --- slither/tools/mutator/__main__.py | 1 + .../mutator/mutators/abstract_mutator.py | 2 +- .../mutator/utils/testing_generated_mutant.py | 23 ++----------------- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index c41a82e35f..8cbb103adc 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -165,6 +165,7 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: + # TODO for M in _get_mutators(mutators_to_run): m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) # check whether the contract instance exists or not diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 5223e2d81f..bd40ad6acd 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -97,7 +97,7 @@ def get_exist_flag(self) -> bool: return self.contract_exist @abc.abstractmethod - def _mutate(self, test_cmd: str, test_dir: str) -> Dict: + def _mutate(self) -> Dict: """TODO Documentation""" return {} diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 6c6e80a556..65cf78c276 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -16,33 +16,14 @@ def compile_generated_mutant(file_path: str, mappings: str) -> bool: crytic_compile.CryticCompile(file_path, solc_remaps=mappings) return True except: # pylint: disable=broad-except - # logger.error("Error Crytic Compile") return False - -# function to run the tests -# def run_test_suite(cmd: str, dir: str) -> bool: -# try: -# # Change to the foundry folder -# # os.chdir(dir) - -# result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) -# # result = subprocess.run(cmd.split(' '), check=True) -# if not result.stderr: -# return True -# except subprocess.CalledProcessError as e: -# logger.error(f"Error executing '{cmd}': {e}") - -# return False -# except Exception as e: -# logger.error(f"An unexpected error occurred: {e}") -# return False def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: # add --fail-fast for foundry tests, to exit after first failure - if "forge test" in cmd and not "--fail-fast" in cmd : + if "forge test" in cmd and not "--fail-fast" in cmd: cmd += " --fail-fast" # add --bail for hardhat and truffle tests, to exit after first failure - elif not "--bail" in cmd: + elif "hardhat test" in cmd or "truffle test" and not "--bail" in cmd: cmd += " --bail" start = time.time() From dfed5431ee694a25af807f0479844e49284cac98 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 17 Jan 2024 11:54:06 -0500 Subject: [PATCH 18/59] updated create patch --- slither/tools/mutator/__main__.py | 30 +++++++++++++++-- slither/tools/mutator/mutators/AOR.py | 4 +-- slither/tools/mutator/mutators/ASOR.py | 4 +-- slither/tools/mutator/mutators/BOR.py | 4 +-- slither/tools/mutator/mutators/CR.py | 32 +++++++++++++++++++ slither/tools/mutator/mutators/FHR.py | 4 +-- slither/tools/mutator/mutators/LIR.py | 6 ++-- slither/tools/mutator/mutators/LOR.py | 4 +-- slither/tools/mutator/mutators/MIA.py | 6 ++-- slither/tools/mutator/mutators/MVIE.py | 6 ++-- slither/tools/mutator/mutators/MVIV.py | 7 ++-- slither/tools/mutator/mutators/MWA.py | 4 +-- slither/tools/mutator/mutators/ROR.py | 4 +-- .../tools/mutator/mutators/{RCR.py => RR.py} | 14 +++----- slither/tools/mutator/mutators/SBR.py | 6 ++-- slither/tools/mutator/mutators/UOR.py | 10 +++--- .../tools/mutator/mutators/all_mutators.py | 5 ++- slither/tools/mutator/utils/file_handling.py | 2 +- slither/tools/mutator/utils/patch.py | 22 +++++++++++++ .../mutator/utils/testing_generated_mutant.py | 2 +- 20 files changed, 125 insertions(+), 51 deletions(-) create mode 100644 slither/tools/mutator/mutators/CR.py rename slither/tools/mutator/mutators/{RCR.py => RR.py} (67%) create mode 100644 slither/tools/mutator/utils/patch.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 8cbb103adc..66c0941a6d 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -83,6 +83,20 @@ def parse_args() -> argparse.Namespace: help="mutant generators to run", ) + # list of contract names you want to mutate + parser.add_argument( + "--contract-names", + help="list of contract names you want to mutate", + ) + + # flag to run full mutation based revert mutator output + parser.add_argument( + "--quick", + help="to stop full mutation if revert mutator passes", + action="store_true", + default=False, + ) + # Initiate all the crytic config cli options cryticparser.init(parser) @@ -118,6 +132,7 @@ def __call__( def main() -> None: args = parse_args() + # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir @@ -127,6 +142,8 @@ def main() -> None: solc_remappings: str | None = args.solc_remaps verbose: bool = args.verbose mutators_to_run: List[str] | None = args.mutators_to_run + contract_names: List[str] | None = args.contract_names + quick_flag: bool = args.quick print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) @@ -150,6 +167,13 @@ def main() -> None: if timeout == None: timeout = 30 + # setting RR mutator as first mutator + mutators_list = _get_mutators(mutators_to_run) + for M in mutators_list: + if M.NAME == "RR": + mutators_list.remove(M) + mutators_list.insert(0, M) + for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] # TODO: user provides contract name @@ -165,14 +189,16 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - # TODO - for M in _get_mutators(mutators_to_run): + for M in mutators_list: m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) # check whether the contract instance exists or not if m.get_exist_flag(): count_valid, count_invalid = m.mutate() v_count += count_valid total_count += count_valid + count_invalid + if quick_flag: + if str(m.NAME) == 'RR' and v_count > 0: + break except Exception as e: logger.error(e) diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 39e3a6a20e..61eed67b1f 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperation @@ -40,5 +40,5 @@ def _mutate(self) -> Dict: line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 04ad8f8b03..e6fa414378 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,5 +1,5 @@ from typing import Dict -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation @@ -44,5 +44,5 @@ def _mutate(self) -> Dict: line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index de9ad287e1..5ca46950fe 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature bitwise_operators = [ @@ -33,5 +33,5 @@ def _mutate(self) -> Dict: line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py new file mode 100644 index 0000000000..1935c43e60 --- /dev/null +++ b/slither/tools/mutator/mutators/CR.py @@ -0,0 +1,32 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.tools.mutator.utils.patch import create_patch_with_line +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature + + +class CR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "CR" + HELP = 'Comment Replacement' + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + new_str = "//" + old_str + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + return result + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 5709346b15..3758306f3f 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -1,5 +1,5 @@ from typing import Dict -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re @@ -31,6 +31,6 @@ def _mutate(self) -> Dict: right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index 99228913aa..c9966f1bec 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -2,7 +2,7 @@ from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.core.solidity_types import ElementaryType literal_replacements = [] @@ -41,7 +41,7 @@ def _mutate(self) -> Dict: old_value = old_str[old_str.find("=")+1:].strip() if old_value != value: new_str = f"{old_str.split('=')[0]}= {value}" - create_patch( + create_patch_with_line( result, self.in_file, start, @@ -69,7 +69,7 @@ def _mutate(self) -> Dict: old_value = old_str[old_str.find("=")+1:].strip() if old_value != new_value: new_str = f"{old_str.split('=')[0]}= {new_value}" - create_patch( + create_patch_with_line( result, self.in_file, start, diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index fa66c7bca2..e6903a01e9 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature logical_operators = [ @@ -32,5 +32,5 @@ def _mutate(self) -> Dict: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index f5bc52b16e..0217324e09 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation @@ -24,11 +24,11 @@ def _mutate(self) -> Dict: # Replace the expression with true and false for value in ["true", "false"]: new_str = value - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) print(node.expression) diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 3c334e9e68..c81182c432 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -2,7 +2,7 @@ from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" @@ -28,7 +28,7 @@ def _mutate(self) -> Dict: new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, @@ -48,7 +48,7 @@ def _mutate(self) -> Dict: new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 7f75b1dcf1..890c55b1cc 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,9 +1,8 @@ from typing import Dict - from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" @@ -28,7 +27,7 @@ def _mutate(self) -> Dict: old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, @@ -46,7 +45,7 @@ def _mutate(self) -> Dict: old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index c6bacf485d..9bb6a16001 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation @@ -23,5 +23,5 @@ def _mutate(self) -> Dict: if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index e1c78c1eb0..2a38ce323b 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature relational_operators = [ @@ -36,6 +36,6 @@ def _mutate(self) -> Dict: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/RCR.py b/slither/tools/mutator/mutators/RR.py similarity index 67% rename from slither/tools/mutator/mutators/RCR.py rename to slither/tools/mutator/mutators/RR.py index 96aebc68f9..95cbf617a7 100644 --- a/slither/tools/mutator/mutators/RCR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -1,12 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -class RCR(AbstractMutator): # pylint: disable=too-few-public-methods - NAME = "RCR" - HELP = 'Revert and Comment Replacement' +class RR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "RR" + HELP = 'Revert Replacement' FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -22,11 +22,7 @@ def _mutate(self) -> Dict: line_no = node.source_mapping.lines if old_str != 'revert()': new_str = 'revert()' - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - new_str = "//" + old_str - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 91c05b884e..9e9afc1e5f 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re from slither.core.variables.variable import Variable @@ -67,7 +67,7 @@ def _mutate(self) -> Dict: right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) for variable in self.contract.state_variables_declared: node = variable.node_initialization @@ -81,7 +81,7 @@ def _mutate(self) -> Dict: right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index 671ee676d5..aac4b02a64 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature unary_operators = [ @@ -36,16 +36,16 @@ def _mutate(self) -> Dict: variable_read = node.variables_read[0] new_str = str(variable_read) + str(op) if new_str != old_str and str(op) != '-': - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(op) + str(variable_read) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) else: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(op) + str(variable_read) if new_str != old_str and str(op) != '-': - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(variable_read) + str(op) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 9aa81a0fc4..21925317dd 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -12,6 +12,5 @@ from slither.tools.mutator.mutators.FHR import FHR # severity medium from slither.tools.mutator.mutators.MIA import MIA # severity medium from slither.tools.mutator.mutators.ROR import ROR # severity medium -from slither.tools.mutator.mutators.RCR import RCR # severity high - - +from slither.tools.mutator.mutators.RR import RR # severity high +from slither.tools.mutator.mutators.CR import CR # severity high \ No newline at end of file diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index d61bba58af..01a142c8d0 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -1,5 +1,5 @@ import os -from typing import Dict, Tuple, List +from typing import Dict, List import logging logger = logging.getLogger("Slither-Mutate") diff --git a/slither/tools/mutator/utils/patch.py b/slither/tools/mutator/utils/patch.py new file mode 100644 index 0000000000..39c77f6731 --- /dev/null +++ b/slither/tools/mutator/utils/patch.py @@ -0,0 +1,22 @@ +from typing import Dict, Union +from collections import defaultdict + +# pylint: disable=too-many-arguments +def create_patch_with_line( + result: Dict, + file: str, + start: int, + end: int, + old_str: Union[str, bytes], + new_str: Union[str, bytes], + line_no: int +) -> None: + if isinstance(old_str, bytes): + old_str = old_str.decode("utf8") + if isinstance(new_str, bytes): + new_str = new_str.decode("utf8") + p = {"start": start, "end": end, "old_string": old_str, "new_string": new_str, "line_number": line_no} + if "patches" not in result: + result["patches"] = defaultdict(list) + if p not in result["patches"][file]: + result["patches"][file].append(p) \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 65cf78c276..8525e28084 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -4,7 +4,7 @@ import logging import time import signal -from typing import List, Dict +from typing import Dict from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file from slither.utils.colors import green, red From 25ed52752d1baa8390599071aba30603cbd997af Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Thu, 18 Jan 2024 10:49:42 -0500 Subject: [PATCH 19/59] Added contract_names arg --- slither/tools/mutator/__main__.py | 16 +++++++---- slither/tools/mutator/mutators/FHR.py | 2 -- .../mutator/mutators/abstract_mutator.py | 28 ++++--------------- .../mutator/utils/testing_generated_mutant.py | 4 ++- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 66c0941a6d..e576970f7f 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -176,7 +176,6 @@ def main() -> None: for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] - # TODO: user provides contract name # slither object sl = Slither(filename, **vars(args)) # create a backup files @@ -189,10 +188,17 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - for M in mutators_list: - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) - # check whether the contract instance exists or not - if m.get_exist_flag(): + contract_instance = '' + for contract in compilation_unit_of_main_file.contracts: + if contract_names != None and contract.name in contract_names: + contract_instance = contract + elif str(contract.name).lower() == contract_name.lower(): + contract_instance = contract + if contract_instance == '': + logger.error("Can't find the contract") + else: + for M in mutators_list: + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder) count_valid, count_invalid = m.mutate() v_count += count_valid total_count += count_valid + count_invalid diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 3758306f3f..a5d7f5f7fc 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -3,8 +3,6 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re -# INFO: low severity - function_header_replacements = [ "pure ==> view", "view ==> pure", diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index bd40ad6acd..edd5843860 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -7,6 +7,7 @@ from slither.formatters.utils.patches import apply_patch, create_diff from slither.tools.mutator.utils.testing_generated_mutant import test_patch from slither.utils.colors import yellow +from slither.core.declarations import Contract logger = logging.getLogger("Slither-Mutate") @@ -36,7 +37,7 @@ def __init__( timeout: int, testing_command: str, testing_directory: str, - contract_name: str, + contract_instance: Contract, solc_remappings: str | None, verbose: bool, output_folder: str, @@ -50,10 +51,12 @@ def __init__( self.test_command = testing_command self.test_directory = testing_directory self.timeout = timeout - self.contract_exist = False self.solc_remappings = solc_remappings self.verbose = verbose self.output_folder = output_folder + self.contract = contract_instance + self.in_file = self.contract.source_mapping.filename.absolute + self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] if not self.NAME: raise IncorrectMutatorInitialization( @@ -74,27 +77,6 @@ def __init__( raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" ) - - # identify the main contract, ignore the imports - for contract in self.slither.contracts: - # !limitation: what if the contract name is not same as file name - # !limitation: multi contract - if contract_name.lower() == str(contract.name).lower(): - # contract - self.contract = contract - # Retrieve the file - self.in_file = self.contract.source_mapping.filename.absolute - # Retrieve the source code - self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] - # flag contract existence - self.contract_exist = True - - if not self.contract_exist: - self.contract_exist = False - logger.error(f"Contract name is not matching with the File name ({contract_name}). Please refer 'https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names')") - - def get_exist_flag(self) -> bool: - return self.contract_exist @abc.abstractmethod def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 8525e28084..2013858df2 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -9,7 +9,7 @@ from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") - +# dont_mutate_line = {} # function to compile the generated mutant def compile_generated_mutant(file_path: str, mappings: str) -> bool: try: @@ -58,6 +58,8 @@ def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) + # if generator_name == 'RR': + # dont_mutate_line[patch['line_number']] = True return True reset_file(file) From 6ae8579b7ef5954b34c24188e083b8d1ec90b5ee Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Sat, 20 Jan 2024 12:25:12 -0500 Subject: [PATCH 20/59] updated quick --- slither/tools/mutator/__main__.py | 22 +++++--- slither/tools/mutator/mutators/AOR.py | 8 +-- slither/tools/mutator/mutators/ASOR.py | 7 ++- slither/tools/mutator/mutators/BOR.py | 7 ++- slither/tools/mutator/mutators/CR.py | 5 +- slither/tools/mutator/mutators/FHR.py | 13 +++-- slither/tools/mutator/mutators/LIR.py | 55 ++++++++++--------- slither/tools/mutator/mutators/LOR.py | 8 +-- slither/tools/mutator/mutators/MIA.py | 20 +++---- slither/tools/mutator/mutators/MVIE.py | 40 +++++++------- slither/tools/mutator/mutators/MVIV.py | 38 +++++++------ slither/tools/mutator/mutators/MWA.py | 8 +-- slither/tools/mutator/mutators/ROR.py | 8 +-- slither/tools/mutator/mutators/RR.py | 7 ++- slither/tools/mutator/mutators/SBR.py | 28 +++++----- slither/tools/mutator/mutators/UOR.py | 35 ++++++------ .../mutator/mutators/abstract_mutator.py | 14 +++-- .../mutator/utils/testing_generated_mutant.py | 4 +- 18 files changed, 172 insertions(+), 155 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index e576970f7f..80d025c970 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -169,10 +169,17 @@ def main() -> None: # setting RR mutator as first mutator mutators_list = _get_mutators(mutators_to_run) - for M in mutators_list: + + CR_RR_list = [] + duplicate_list = mutators_list.copy() + for M in duplicate_list: if M.NAME == "RR": mutators_list.remove(M) - mutators_list.insert(0, M) + CR_RR_list.insert(0,M) + elif M.NAME == "CR": + mutators_list.remove(M) + CR_RR_list.insert(1,M) + mutators_list = CR_RR_list + mutators_list for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] @@ -185,6 +192,7 @@ def main() -> None: # count of valid mutants v_count = 0 + dont_mutate_lines = [] # mutation try: for compilation_unit_of_main_file in sl.compilation_units: @@ -198,13 +206,13 @@ def main() -> None: logger.error("Can't find the contract") else: for M in mutators_list: - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder) - count_valid, count_invalid = m.mutate() + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder, dont_mutate_lines) + (count_valid, count_invalid, lines_list) = m.mutate() v_count += count_valid total_count += count_valid + count_invalid - if quick_flag: - if str(m.NAME) == 'RR' and v_count > 0: - break + dont_mutate_lines = lines_list + if not quick_flag: + dont_mutate_lines = [] except Exception as e: logger.error(e) diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 61eed67b1f..11593ee372 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -19,7 +19,6 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: try: @@ -38,7 +37,8 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index e6fa414378..29a6dc6726 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -42,7 +42,8 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 5ca46950fe..6a5552a5f5 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -31,7 +31,8 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 1935c43e60..79a63a88fb 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -20,8 +20,9 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - new_str = "//" + old_str - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + new_str = "//" + old_str + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index a5d7f5f7fc..84bfebe227 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -24,11 +24,12 @@ def _mutate(self) -> Dict: stop = start + function.source_mapping.content.find('{') old_str = self.in_file_str[start:stop] line_no = function.source_mapping.lines - for value in function_header_replacements: - left_value = value.split(" ==> ")[0] - right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + for value in function_header_replacements: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index c9966f1bec..e8217e9355 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -36,20 +36,20 @@ def _mutate(self) -> Dict: stop = start + variable.source_mapping.length old_str = self.in_file_str[start:stop] line_no = variable.node_initialization.source_mapping.lines - # line_no = [0] - for value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() - if old_value != value: - new_str = f"{old_str.split('=')[0]}= {value}" - create_patch_with_line( - result, - self.in_file, - start, - stop, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + for value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != value: + new_str = f"{old_str.split('=')[0]}= {value}" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: @@ -65,18 +65,19 @@ def _mutate(self) -> Dict: stop = start + variable.source_mapping.length old_str = self.in_file_str[start:stop] line_no = variable.source_mapping.lines - for new_value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() - if old_value != new_value: - new_str = f"{old_str.split('=')[0]}= {new_value}" - create_patch_with_line( - result, - self.in_file, - start, - stop, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + for new_value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != new_value: + new_str = f"{old_str.split('=')[0]}= {new_value}" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index e6903a01e9..e14c85708f 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -29,8 +29,8 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 0217324e09..421f94a5e5 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -20,16 +20,14 @@ def _mutate(self) -> Dict: stop = start + node.expression.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true and false + for value in ["true", "false"]: + new_str = value + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - # Replace the expression with true and false - for value in ["true", "false"]: - new_str = value - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - print(node.expression) - return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index c81182c432..4ae387d294 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -25,18 +25,18 @@ def _mutate(self) -> Dict: start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: @@ -45,17 +45,17 @@ def _mutate(self) -> Dict: start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 890c55b1cc..68bc2f2d83 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -27,15 +27,16 @@ def _mutate(self) -> Dict: old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: @@ -45,14 +46,15 @@ def _mutate(self) -> Dict: old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 9bb6a16001..4c1e91b324 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -20,8 +20,8 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - - if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 2a38ce323b..da75a592ab 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -33,9 +33,9 @@ def _mutate(self) -> Dict: stop = start + ir.expression.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 95cbf617a7..3aab255eca 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -20,9 +20,10 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - if old_str != 'revert()': - new_str = 'revert()' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + if old_str != 'revert()': + new_str = 'revert()' + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 9e9afc1e5f..f6adfff845 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -56,18 +56,19 @@ def _mutate(self) -> Dict: for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type != NodeType.ENTRYPOINT: + if node.type != NodeType.ENTRYPOINT and node.type != NodeType.ENDIF and node.type != NodeType.ENDLOOP: # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - for value in solidity_rules: - left_value = value.split(" ==> ")[0] - right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) for variable in self.contract.state_variables_declared: node = variable.node_initialization @@ -76,12 +77,13 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - for value in solidity_rules: - left_value = value.split(" ==> ")[0] - right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index aac4b02a64..fa00b52ae7 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -29,23 +29,24 @@ def _mutate(self) -> Dict: stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: - for op in unary_operators: - if not node.expression.is_prefix: - if node.expression.type != op: - variable_read = node.variables_read[0] - new_str = str(variable_read) + str(op) - if new_str != old_str and str(op) != '-': + if not line_no[0] in self.dont_mutate_line: + if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: + for op in unary_operators: + if not node.expression.is_prefix: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(variable_read) + str(op) + if new_str != old_str and str(op) != '-': + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(op) + str(variable_read) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - new_str = str(op) + str(variable_read) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - else: - if node.expression.type != op: - variable_read = node.variables_read[0] - new_str = str(op) + str(variable_read) - if new_str != old_str and str(op) != '-': + else: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(op) + str(variable_read) + if new_str != old_str and str(op) != '-': + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(variable_read) + str(op) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - new_str = str(variable_read) + str(op) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index edd5843860..f11d69db1d 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 enum import Enum -from typing import Optional, Dict, Tuple +from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit # from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff @@ -41,6 +41,7 @@ def __init__( solc_remappings: str | None, verbose: bool, output_folder: str, + dont_mutate_line: List[int], rate: int = 10, seed: Optional[int] = None ) -> None: @@ -57,6 +58,7 @@ def __init__( self.contract = contract_instance self.in_file = self.contract.source_mapping.filename.absolute self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] + self.dont_mutate_line = dont_mutate_line if not self.NAME: raise IncorrectMutatorInitialization( @@ -83,13 +85,12 @@ def _mutate(self) -> Dict: """TODO Documentation""" return {} - def mutate(self) -> Tuple[int, int]: + def mutate(self) -> Tuple[int, int, List[int]]: # call _mutate function from different mutators (all_patches) = self._mutate() - if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") - return (0,0) + return (0,0,self.dont_mutate_line) for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") @@ -99,6 +100,8 @@ def mutate(self) -> Tuple[int, int]: for patch in patches: # test the patch flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) + if (self.NAME == 'RR' or self.NAME == 'CR') and flag: + self.dont_mutate_line.append(patch['line_number']) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 @@ -112,8 +115,7 @@ def mutate(self) -> Tuple[int, int]: # add valid mutant patches to a output file with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: patches_file.write(diff + '\n') - - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 2013858df2..8525e28084 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -9,7 +9,7 @@ from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") -# dont_mutate_line = {} + # function to compile the generated mutant def compile_generated_mutant(file_path: str, mappings: str) -> bool: try: @@ -58,8 +58,6 @@ def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) - # if generator_name == 'RR': - # dont_mutate_line[patch['line_number']] = True return True reset_file(file) From 9ce98f4d2656a8c9aad5926d3ec2bb34f32117c5 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Sun, 21 Jan 2024 12:08:13 -0500 Subject: [PATCH 21/59] Added README --- slither/tools/mutator/README.md | 31 +++++++++++++++++++ slither/tools/mutator/__main__.py | 12 ++++--- .../mutator/mutators/abstract_mutator.py | 2 +- slither/tools/mutator/utils/file_handling.py | 4 +-- 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 slither/tools/mutator/README.md diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md new file mode 100644 index 0000000000..11b0b7eb51 --- /dev/null +++ b/slither/tools/mutator/README.md @@ -0,0 +1,31 @@ +# Slither-mutate + +`slither-mutate` is a mutation testing tool for solidity based smart contracts. + +## Usage + +`slither-mutate ` + +### CLI Interface + +``` +positional arguments: + codebase Codebase to analyze (.sol file, truffle directory, ...) + test-cmd Command to run the tests for your project + +options: + -h, --help show this help message and exit + --list-mutators List available detectors + --test-dir TEST_DIR Tests directory + --ignore-dirs IGNORE_DIRS + Directories to ignore + --timeout TIMEOUT Set timeout for test command (by default 30 seconds) + --output-dir OUTPUT_DIR + Name of output Directory (by default 'mutation_campaign') + --verbose output all mutants generated + --mutators-to-run MUTATORS_TO_RUN + mutant generators to run + --contract-names CONTRACT_NAMES + list of contract names you want to mutate + --quick to stop full mutation if revert mutator passes +``` \ No newline at end of file diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 80d025c970..b375e9558a 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -41,14 +41,14 @@ def parse_args() -> argparse.Namespace: # argument to add the test command parser.add_argument( - "--test-cmd", - help="Command line needed to run the tests for your project" + "test-cmd", + help="Command to run the tests for your project" ) # argument to add the test directory - containing all the tests parser.add_argument( "--test-dir", - help="Directory of tests" + help="Tests directory" ) # argument to ignore the interfaces, libraries @@ -66,7 +66,7 @@ def parse_args() -> argparse.Namespace: # output directory argument parser.add_argument( "--output-dir", - help="Output Directory (by default 'mutation_campaign')" + help="Name of output directory (by default 'mutation_campaign')" ) # to print just all the mutants @@ -170,6 +170,7 @@ def main() -> None: # setting RR mutator as first mutator mutators_list = _get_mutators(mutators_to_run) + # insert RR and CR in front of the list CR_RR_list = [] duplicate_list = mutators_list.copy() for M in duplicate_list: @@ -191,8 +192,9 @@ def main() -> None: total_count = 0 # count of valid mutants v_count = 0 - + # lines those need not be mutated (taken from RR and CR) dont_mutate_lines = [] + # mutation try: for compilation_unit_of_main_file in sl.compilation_units: diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index f11d69db1d..558a514513 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -3,7 +3,6 @@ from enum import Enum from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit -# from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff from slither.tools.mutator.utils.testing_generated_mutant import test_patch from slither.utils.colors import yellow @@ -100,6 +99,7 @@ def mutate(self) -> Tuple[int, int, List[int]]: for patch in patches: # test the patch flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) + # if RR or CR and valid mutant, add line no. if (self.NAME == 'RR' or self.NAME == 'CR') and flag: self.dont_mutate_line.append(patch['line_number']) # count the valid and invalid mutants diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index 01a142c8d0..ffebee9fc9 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -104,6 +104,4 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str for i in get_sol_file_list(filename, ignore_paths): sol_file_list.append(i) - return sol_file_list - -# TODO: create a function to delete the commands from the sol file + return sol_file_list \ No newline at end of file From 3db8da27c3fcf6caff911230e0643b33b7a5034c Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 24 Jan 2024 11:47:55 -0500 Subject: [PATCH 22/59] Updated arguments --- slither/tools/mutator/README.md | 6 +++--- slither/tools/mutator/__main__.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md index 11b0b7eb51..c615b0b795 100644 --- a/slither/tools/mutator/README.md +++ b/slither/tools/mutator/README.md @@ -4,24 +4,24 @@ ## Usage -`slither-mutate ` +`slither-mutate --test-cmd ` ### CLI Interface ``` positional arguments: codebase Codebase to analyze (.sol file, truffle directory, ...) - test-cmd Command to run the tests for your project options: -h, --help show this help message and exit --list-mutators List available detectors + --test-cmd TEST_CMD Command to run the tests for your project --test-dir TEST_DIR Tests directory --ignore-dirs IGNORE_DIRS Directories to ignore --timeout TIMEOUT Set timeout for test command (by default 30 seconds) --output-dir OUTPUT_DIR - Name of output Directory (by default 'mutation_campaign') + Name of output directory (by default 'mutation_campaign') --verbose output all mutants generated --mutators-to-run MUTATORS_TO_RUN mutant generators to run diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index b375e9558a..7bd4f8d791 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -26,7 +26,7 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597", - usage="slither-mutate target", + usage="slither-mutate --test-cmd ", ) parser.add_argument("codebase", help="Codebase to analyze (.sol file, truffle directory, ...)") @@ -41,7 +41,7 @@ def parse_args() -> argparse.Namespace: # argument to add the test command parser.add_argument( - "test-cmd", + "--test-cmd", help="Command to run the tests for your project" ) From a51fd70bb12c4b646f560d9ba7650856b7f6b0e7 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 24 Jan 2024 14:49:28 -0500 Subject: [PATCH 23/59] Updated mutators --- slither/tools/mutator/README.md | 2 ++ slither/tools/mutator/__main__.py | 2 +- slither/tools/mutator/mutators/AOR.py | 3 +-- slither/tools/mutator/mutators/ASOR.py | 3 +-- slither/tools/mutator/mutators/BOR.py | 3 +-- slither/tools/mutator/mutators/CR.py | 3 +-- slither/tools/mutator/mutators/FHR.py | 3 +-- slither/tools/mutator/mutators/LIR.py | 3 +-- slither/tools/mutator/mutators/LOR.py | 3 +-- slither/tools/mutator/mutators/MIA.py | 3 +-- slither/tools/mutator/mutators/MVIE.py | 3 +-- slither/tools/mutator/mutators/MVIV.py | 3 +-- slither/tools/mutator/mutators/MWA.py | 3 +-- slither/tools/mutator/mutators/ROR.py | 3 +-- slither/tools/mutator/mutators/RR.py | 4 +--- slither/tools/mutator/mutators/SBR.py | 3 +-- slither/tools/mutator/mutators/UOR.py | 3 +-- .../mutator/mutators/abstract_mutator.py | 24 +------------------ slither/tools/mutator/utils/command_line.py | 13 +++++----- 19 files changed, 25 insertions(+), 62 deletions(-) diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md index c615b0b795..30b83ac460 100644 --- a/slither/tools/mutator/README.md +++ b/slither/tools/mutator/README.md @@ -6,6 +6,8 @@ `slither-mutate --test-cmd ` +To view the list of mutators available `slither-mutate --list-mutators` + ### CLI Interface ``` diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 7bd4f8d791..13e78a30ec 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -118,7 +118,7 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def __call__( self, parser: Any, *args: Any, **kwargs: Any ) -> None: # pylint: disable=signature-differs - checks = _get_mutators() + checks = _get_mutators(None) output_mutators(checks) parser.exit() diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 11593ee372..0c7f906b38 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -1,7 +1,7 @@ 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, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperation arithmetic_operators = [ @@ -15,7 +15,6 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "AOR" HELP = "Arithmetic operator replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 29a6dc6726..551cf8c644 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation assignment_operators = [ @@ -20,7 +20,6 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ASOR" HELP = "Assignment Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 6a5552a5f5..b6a9ae749c 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -1,7 +1,7 @@ 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, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator bitwise_operators = [ BinaryType.AND, @@ -14,7 +14,6 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "BOR" HELP = "Bitwise Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 79a63a88fb..5c34416e9f 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator class CR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "CR" HELP = 'Comment Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 84bfebe227..847466611c 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator import re function_header_replacements = [ @@ -13,7 +13,6 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "FHR" HELP = 'Function Header Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index e8217e9355..29efe6502c 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line from slither.core.solidity_types import ElementaryType @@ -10,7 +10,6 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LIR" HELP = "Literal Interger Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index e14c85708f..a8fff1c31f 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -1,7 +1,7 @@ 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, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator logical_operators = [ BinaryType.OROR, @@ -11,7 +11,6 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LOR" HELP = "Logical Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 421f94a5e5..5920143499 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 4ae387d294..b562ba9b0b 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 68bc2f2d83..dcb5be2592 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 4c1e91b324..b482ab5661 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MWA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MWA" HELP = '"while" construct around statement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index da75a592ab..826b15a4ad 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,7 +1,7 @@ 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, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator relational_operators = [ BinaryType.LESS, @@ -15,7 +15,6 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ROR" HELP = "Relational Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 3aab255eca..5afe188928 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -1,13 +1,11 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature - +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator class RR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "RR" HELP = 'Revert Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index f6adfff845..0e94329089 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator import re from slither.core.variables.variable import Variable @@ -47,7 +47,6 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" HELP = 'Solidity Based Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index fa00b52ae7..cf62c21bd1 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator unary_operators = [ UnaryOperationType.PLUSPLUS_PRE, @@ -14,7 +14,6 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "UOR" HELP = "Unary Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 558a514513..ea331e7c5c 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,6 +1,5 @@ import abc import logging -from enum import Enum from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit from slither.formatters.utils.patches import apply_patch, create_diff @@ -12,22 +11,10 @@ class IncorrectMutatorInitialization(Exception): pass - -class FaultNature(Enum): - Missing = 0 - Wrong = 1 - Extraneous = 2 - Undefined = 100 - - # not executed - can be detected by replacing with revert - # has no effect - can be detected by removing a line / comment - # can have valid mutant - # can't have valid mutant class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods NAME = "" HELP = "" - FAULTNATURE = FaultNature.Undefined VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 @@ -69,11 +56,6 @@ def __init__( f"HELP is not initialized {self.__class__.__name__}" ) - if self.FAULTNATURE == FaultNature.Undefined: - raise IncorrectMutatorInitialization( - f"FAULTNATURE is not initialized {self.__class__.__name__}" - ) - if rate < 0 or rate > 100: raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" @@ -115,8 +97,4 @@ def mutate(self) -> Tuple[int, int, List[int]]: # add valid mutant patches to a output file with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: patches_file.write(diff + '\n') - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) - - - - \ No newline at end of file + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) \ No newline at end of file diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index 042b4fff70..358586688c 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -7,14 +7,13 @@ def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: for detector in mutators_classes: argument = detector.NAME help_info = detector.HELP - fault_nature = detector.FAULTNATURE.name - mutators_list.append((argument, help_info, fault_nature)) - table = MyPrettyTable(["Num", "Name", "What it Does", "Fault Nature"]) + mutators_list.append((argument, help_info)) + table = MyPrettyTable(["Num", "Name", "What it Does"]) - # Sort by class, nature, name - mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[0])) + # Sort by class + mutators_list = sorted(mutators_list, key=lambda element: (element[0])) idx = 1 - for (argument, help_info, fault_nature) in mutators_list: - table.add_row([str(idx), argument, help_info, fault_nature]) + for (argument, help_info) in mutators_list: + table.add_row([str(idx), argument, help_info]) idx = idx + 1 print(table) From 6f584ece652a98009a98d33360ac3b6e8bfafbe3 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Thu, 25 Jan 2024 12:46:34 -0500 Subject: [PATCH 24/59] Updated files --- slither/tools/mutator/README.md | 8 +-- slither/tools/mutator/__main__.py | 66 ++++++++++--------- slither/tools/mutator/mutators/AOR.py | 6 +- slither/tools/mutator/mutators/ASOR.py | 6 +- slither/tools/mutator/mutators/BOR.py | 4 +- slither/tools/mutator/mutators/CR.py | 13 +--- slither/tools/mutator/mutators/FHR.py | 13 ++-- slither/tools/mutator/mutators/LIR.py | 7 +- slither/tools/mutator/mutators/LOR.py | 4 +- slither/tools/mutator/mutators/MIA.py | 7 +- slither/tools/mutator/mutators/MVIE.py | 3 +- slither/tools/mutator/mutators/MVIV.py | 3 +- slither/tools/mutator/mutators/MWA.py | 7 +- slither/tools/mutator/mutators/ROR.py | 3 +- slither/tools/mutator/mutators/RR.py | 10 +-- slither/tools/mutator/mutators/SBR.py | 20 ++---- slither/tools/mutator/mutators/UOR.py | 7 +- .../mutator/mutators/abstract_mutator.py | 30 ++++----- .../tools/mutator/mutators/all_mutators.py | 4 +- slither/tools/mutator/utils/file_handling.py | 60 +++++++++-------- slither/tools/mutator/utils/patch.py | 2 +- .../mutator/utils/testing_generated_mutant.py | 54 ++++++++++----- 22 files changed, 168 insertions(+), 169 deletions(-) diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md index 30b83ac460..8af977b082 100644 --- a/slither/tools/mutator/README.md +++ b/slither/tools/mutator/README.md @@ -1,6 +1,6 @@ # Slither-mutate -`slither-mutate` is a mutation testing tool for solidity based smart contracts. +`slither-mutate` is a mutation testing tool for solidity based smart contracts. ## Usage @@ -10,9 +10,9 @@ To view the list of mutators available `slither-mutate --list-mutators` ### CLI Interface -``` +```shell positional arguments: - codebase Codebase to analyze (.sol file, truffle directory, ...) + codebase Codebase to analyze (.sol file, project directory, ...) options: -h, --help show this help message and exit @@ -30,4 +30,4 @@ options: --contract-names CONTRACT_NAMES list of contract names you want to mutate --quick to stop full mutation if revert mutator passes -``` \ No newline at end of file +``` diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 13e78a30ec..56582d6f54 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -4,14 +4,14 @@ import sys import os import shutil -from typing import Type, List, Any +from typing import Type, List, Any, Optional from crytic_compile import cryticparser from slither import Slither from slither.tools.mutator.mutators import all_mutators +from slither.utils.colors import yellow, magenta from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list -from slither.utils.colors import yellow, magenta logging.basicConfig() logger = logging.getLogger("Slither-Mutate") @@ -24,12 +24,16 @@ ################################################################################### def parse_args() -> argparse.Namespace: + """ + Parse the underlying arguments for the program. + Returns: The arguments for the program. + """ parser = argparse.ArgumentParser( description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597", usage="slither-mutate --test-cmd ", ) - parser.add_argument("codebase", help="Codebase to analyze (.sol file, truffle directory, ...)") + parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)") parser.add_argument( "--list-mutators", @@ -108,7 +112,7 @@ def parse_args() -> argparse.Namespace: def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] - if not mutators_list is None: + if mutators_list is not None: detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) and str(c.NAME) in mutators_list ] else: detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) ] @@ -122,7 +126,6 @@ def __call__( output_mutators(checks) parser.exit() - # endregion ################################################################################### ################################################################################### @@ -130,46 +133,46 @@ def __call__( ################################################################################### ################################################################################### -def main() -> None: +def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too-many-locals args = parse_args() # arguments test_command: str = args.test_cmd - test_directory: str = args.test_dir - paths_to_ignore: str | None = args.ignore_dirs - output_dir: str | None = args.output_dir - timeout: int | None = args.timeout - solc_remappings: str | None = args.solc_remaps - verbose: bool = args.verbose - mutators_to_run: List[str] | None = args.mutators_to_run - contract_names: List[str] | None = args.contract_names - quick_flag: bool = args.quick - - print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) + test_directory: Optional[str] = args.test_dir + paths_to_ignore: Optional[str] = args.ignore_dirs + output_dir: Optional[str] = args.output_dir + timeout: Optional[int] = args.timeout + solc_remappings: Optional[str] = args.solc_remaps + verbose: Optional[bool] = args.verbose + mutators_to_run: Optional[List[str]] = args.mutators_to_run + contract_names: Optional[List[str]] = args.contract_names + quick_flag: Optional[bool] = args.quick + + logger.info(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) if paths_to_ignore: paths_to_ignore_list = paths_to_ignore.strip('][').split(',') - print(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) + logger.info(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) else: paths_to_ignore_list = [] - # get all the contracts as a list from given codebase + # get all the contracts as a list from given codebase sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore_list) # folder where backup files and valid mutants created - if output_dir == None: + if output_dir is None: output_dir = "/mutation_campaign" output_folder = os.getcwd() + output_dir if os.path.exists(output_folder): shutil.rmtree(output_folder) # set default timeout - if timeout == None: + if timeout is None: timeout = 30 # setting RR mutator as first mutator mutators_list = _get_mutators(mutators_to_run) - + # insert RR and CR in front of the list CR_RR_list = [] duplicate_list = mutators_list.copy() @@ -178,15 +181,15 @@ def main() -> None: mutators_list.remove(M) CR_RR_list.insert(0,M) elif M.NAME == "CR": - mutators_list.remove(M) + mutators_list.remove(M) CR_RR_list.insert(1,M) mutators_list = CR_RR_list + mutators_list - for filename in sol_file_list: + for filename in sol_file_list: # pylint: disable=too-many-nested-blocks contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) - # create a backup files + # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) # total count of mutants total_count = 0 @@ -200,7 +203,7 @@ def main() -> None: for compilation_unit_of_main_file in sl.compilation_units: contract_instance = '' for contract in compilation_unit_of_main_file.contracts: - if contract_names != None and contract.name in contract_names: + if contract_names is not None and contract.name in contract_names: contract_instance = contract elif str(contract.name).lower() == contract_name.lower(): contract_instance = contract @@ -215,20 +218,19 @@ def main() -> None: dont_mutate_lines = lines_list if not quick_flag: dont_mutate_lines = [] - except Exception as e: + except Exception as e: # pylint: disable=broad-except logger.error(e) except KeyboardInterrupt: # transfer and delete the backup files if interrupted logger.error("\nExecution interrupted by user (Ctrl + C). Cleaning up...") transfer_and_delete(files_dict) - + # transfer and delete the backup files transfer_and_delete(files_dict) - + # output - print(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) + logger.info(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) - print(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) + logger.info(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) # endregion - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 0c7f906b38..00506a1bbb 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -18,11 +18,11 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: try: ir_expression = node.expression - except: + except: # pylint: disable=bare-except continue for ir in node.irs: if isinstance(ir, Binary) and ir.type in arithmetic_operators: @@ -40,4 +40,4 @@ def _mutate(self) -> Dict: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 551cf8c644..0e7452594f 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -24,7 +24,7 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir.expression, AssignmentOperation) and ir.expression.type in assignment_operators: @@ -33,7 +33,7 @@ def _mutate(self) -> Dict: alternative_ops = assignment_operators[:] try: alternative_ops.remove(ir.expression.type) - except: + except: # pylint: disable=bare-except continue for op in alternative_ops: if op != ir.expression: @@ -45,4 +45,4 @@ def _mutate(self) -> Dict: # Replace the expression with true new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index b6a9ae749c..9f8ca2a118 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -18,7 +18,7 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in bitwise_operators: @@ -34,4 +34,4 @@ def _mutate(self) -> Dict: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 5c34416e9f..2960282362 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -11,22 +11,15 @@ class CR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: - if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: new_str = "//" + old_str create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result - - - - - - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 847466611c..4560db4da2 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -1,7 +1,8 @@ from typing import Dict +import re from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator -import re + function_header_replacements = [ "pure ==> view", @@ -16,9 +17,8 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - - for function in self.contract.functions_and_modifiers_declared: - # function_header = function.source_mapping.content.split('{')[0] + + for function in self.contract.functions_and_modifiers_declared: start = function.source_mapping.start stop = start + function.source_mapping.content.find('{') old_str = self.in_file_str[start:stop] @@ -27,8 +27,7 @@ def _mutate(self) -> Dict: for value in function_header_replacements: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: + if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index 29efe6502c..f9ef874cd9 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -11,12 +11,12 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LIR" HELP = "Literal Interger Replacement" - def _mutate(self) -> Dict: + def _mutate(self) -> Dict: # pylint: disable=too-many-branches result: Dict = {} variable: Variable # Create fault for state variables declaration - for variable in self.contract.state_variables_declared: + for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: @@ -50,7 +50,7 @@ def _mutate(self) -> Dict: line_no[0] ) - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for variable in function.local_variables: if variable.initialized and isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): @@ -78,5 +78,4 @@ def _mutate(self) -> Dict: new_str, line_no[0] ) - return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index a8fff1c31f..21837d81e7 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -15,7 +15,7 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in logical_operators: @@ -32,4 +32,4 @@ def _mutate(self) -> Dict: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 5920143499..a10ce1f1d7 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -10,7 +10,6 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type == NodeType.IF: @@ -24,9 +23,9 @@ def _mutate(self) -> Dict: for value in ["true", "false"]: new_str = value create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - + if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + return result + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index b562ba9b0b..678b3b7475 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -56,5 +56,4 @@ def _mutate(self) -> Dict: new_str, line_no[0] ) - - return result + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index dcb5be2592..c82f132d74 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -55,5 +55,4 @@ def _mutate(self) -> Dict: new_str, line_no[0] ) - - return result + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index b482ab5661..20447290aa 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -10,7 +10,7 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type == NodeType.IFLOOP: @@ -22,5 +22,6 @@ def _mutate(self) -> Dict: if not line_no[0] in self.dont_mutate_line: if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 826b15a4ad..d59e7575a9 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -19,7 +19,7 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in relational_operators: @@ -37,4 +37,3 @@ def _mutate(self) -> Dict: new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 5afe188928..6bfd644e05 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -12,20 +12,14 @@ def _mutate(self) -> Dict: for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: if old_str != 'revert()': new_str = 'revert()' create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - - - - - - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 0e94329089..bbdc631943 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,8 +1,8 @@ from typing import Dict +import re from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator -import re from slither.core.variables.variable import Variable solidity_rules = [ @@ -43,19 +43,17 @@ "while ==> if", ] - class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" HELP = 'Solidity Based Replacement' def _mutate(self) -> Dict: - result: Dict = {} variable: Variable - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: - if node.type != NodeType.ENTRYPOINT and node.type != NodeType.ENDIF and node.type != NodeType.ENDLOOP: + if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length @@ -65,11 +63,11 @@ def _mutate(self) -> Dict: for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: + if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - for variable in self.contract.state_variables_declared: + for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks node = variable.node_initialization if node: start = node.source_mapping.start @@ -80,13 +78,7 @@ def _mutate(self) -> Dict: for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: + if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - - - - - - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index cf62c21bd1..b7e8c81569 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -18,11 +18,11 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: try: ir_expression = node.expression - except: + except: # pylint: disable=bare-except continue start = node.source_mapping.start stop = start + node.source_mapping.length @@ -47,5 +47,4 @@ def _mutate(self) -> Dict: create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(variable_read) + str(op) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index ea331e7c5c..c2f15cd748 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -11,24 +11,24 @@ class IncorrectMutatorInitialization(Exception): pass - + class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods NAME = "" HELP = "" VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 - def __init__( - self, compilation_unit: SlitherCompilationUnit, - timeout: int, - testing_command: str, - testing_directory: str, - contract_instance: Contract, - solc_remappings: str | None, + def __init__( # pylint: disable=too-many-arguments + self, compilation_unit: SlitherCompilationUnit, + timeout: int, + testing_command: str, + testing_directory: str, + contract_instance: Contract, + solc_remappings: str | None, verbose: bool, output_folder: str, dont_mutate_line: List[int], - rate: int = 10, + rate: int = 10, seed: Optional[int] = None ) -> None: self.compilation_unit = compilation_unit @@ -60,7 +60,7 @@ def __init__( raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" ) - + @abc.abstractmethod def _mutate(self) -> Dict: """TODO Documentation""" @@ -70,19 +70,19 @@ def mutate(self) -> Tuple[int, int, List[int]]: # call _mutate function from different mutators (all_patches) = self._mutate() if "patches" not in all_patches: - logger.debug(f"No patches found by {self.NAME}") + logger.debug("No patches found by %s", self.NAME) return (0,0,self.dont_mutate_line) - + for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") patches = all_patches["patches"][file] patches.sort(key=lambda x: x["start"]) - print(yellow(f"Mutating {file} with {self.NAME} \n")) + logger.info(yellow(f"Mutating {file} with {self.NAME} \n")) for patch in patches: # test the patch flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) # if RR or CR and valid mutant, add line no. - if (self.NAME == 'RR' or self.NAME == 'CR') and flag: + if self.NAME in ('RR', 'CR') and flag: self.dont_mutate_line.append(patch['line_number']) # count the valid and invalid mutants if not flag: @@ -97,4 +97,4 @@ def mutate(self) -> Tuple[int, int, List[int]]: # add valid mutant patches to a output file with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: patches_file.write(diff + '\n') - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) \ No newline at end of file + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 21925317dd..852a8efc34 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -3,7 +3,7 @@ from slither.tools.mutator.mutators.MVIE import MVIE # severity low from slither.tools.mutator.mutators.LOR import LOR # severity medium from slither.tools.mutator.mutators.UOR import UOR # severity medium -from slither.tools.mutator.mutators.SBR import SBR # severity medium +from slither.tools.mutator.mutators.SBR import SBR # severity medium from slither.tools.mutator.mutators.AOR import AOR # severity medium from slither.tools.mutator.mutators.BOR import BOR # severity medium from slither.tools.mutator.mutators.ASOR import ASOR # severity medium @@ -13,4 +13,4 @@ from slither.tools.mutator.mutators.MIA import MIA # severity medium from slither.tools.mutator.mutators.ROR import ROR # severity medium from slither.tools.mutator.mutators.RR import RR # severity high -from slither.tools.mutator.mutators.CR import CR # severity high \ No newline at end of file +from slither.tools.mutator.mutators.CR import CR # severity high diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index ffebee9fc9..d4474c3101 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -6,47 +6,49 @@ duplicated_files = {} -# function to backup the source file def backup_source_file(source_code: Dict, output_folder: str) -> Dict: + """ + function to backup the source file + returns: dictionary of duplicated files + """ os.makedirs(output_folder, exist_ok=True) - + for file_path, content in source_code.items(): directory, filename = os.path.split(file_path) new_filename = f"{output_folder}/backup_{filename}" new_file_path = os.path.join(directory, new_filename) - with open(new_file_path, 'w') as new_file: + with open(new_file_path, 'w', encoding="utf8") as new_file: new_file.write(content) duplicated_files[file_path] = new_file_path return duplicated_files -# function to transfer the original content to the sol file after campaign def transfer_and_delete(files_dict: Dict) -> None: + """function to transfer the original content to the sol file after campaign""" try: files_dict_copy = files_dict.copy() - for item, value in files_dict_copy.items(): - with open(value, 'r') as duplicated_file: + for item, value in files_dict_copy.items(): + with open(value, 'r', encoding="utf8") as duplicated_file: content = duplicated_file.read() - with open(item, 'w') as original_file: + with open(item, 'w', encoding="utf8") as original_file: original_file.write(content) os.remove(value) # delete elements from the global dict del duplicated_files[item] - - except Exception as e: + + except Exception as e: # pylint: disable=broad-except logger.error(f"Error transferring content: {e}") -#function to create new mutant file def create_mutant_file(file: str, count: int, rule: str) -> None: - + """function to create new mutant file""" try: - directory, filename = os.path.split(file) + _, filename = os.path.split(file) # Read content from the duplicated file - with open(file, 'r') as source_file: + with open(file, 'r', encoding="utf8") as source_file: content = source_file.read() # Write content to the original file @@ -54,42 +56,46 @@ def create_mutant_file(file: str, count: int, rule: str) -> None: # create folder for each contract os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) - with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w', encoding="utf8") as mutant_file: mutant_file.write(content) # reset the file - with open(duplicated_files[file], 'r') as duplicated_file: + with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w') as source_file: + with open(file, 'w', encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: + except Exception as e: # pylint: disable=broad-except logger.error(f"Error creating mutant: {e}") -# function to reset the file def reset_file(file: str) -> None: + """function to reset the file""" try: # directory, filename = os.path.split(file) # reset the file - with open(duplicated_files[file], 'r') as duplicated_file: + with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w') as source_file: + with open(file, 'w', encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: + except Exception as e: # pylint: disable=broad-except logger.error(f"Error resetting file: {e}") -# function to get the contracts list + def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str]: + """ + function to get the contracts list + returns: list of .sol files + """ sol_file_list = [] - if ignore_paths == None: + if ignore_paths is None: ignore_paths = [] # if input is contract file if os.path.isfile(codebase): return [codebase] - + # if input is folder elif os.path.isdir(codebase): directory = os.path.abspath(codebase) @@ -98,10 +104,10 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str if os.path.isfile(filename): sol_file_list.append(filename) elif os.path.isdir(filename): - directory_name, dirname = os.path.split(filename) + _, dirname = os.path.split(filename) if dirname in ignore_paths: - continue + continue for i in get_sol_file_list(filename, ignore_paths): sol_file_list.append(i) - return sol_file_list \ No newline at end of file + return sol_file_list diff --git a/slither/tools/mutator/utils/patch.py b/slither/tools/mutator/utils/patch.py index 39c77f6731..e50f44612b 100644 --- a/slither/tools/mutator/utils/patch.py +++ b/slither/tools/mutator/utils/patch.py @@ -19,4 +19,4 @@ def create_patch_with_line( if "patches" not in result: result["patches"] = defaultdict(list) if p not in result["patches"][file]: - result["patches"][file].append(p) \ No newline at end of file + result["patches"][file].append(p) diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 8525e28084..5c75f0eb31 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -1,34 +1,48 @@ -import crytic_compile import subprocess import os import logging import time import signal from typing import Dict +import crytic_compile from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") -# function to compile the generated mutant def compile_generated_mutant(file_path: str, mappings: str) -> bool: + """ + function to compile the generated mutant + returns: status of compilation + """ try: crytic_compile.CryticCompile(file_path, solc_remaps=mappings) return True - except: # pylint: disable=broad-except + except: # pylint: disable=bare-except return False - + def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: + """ + function to run codebase tests + returns: boolean whether the tests passed or not + """ # add --fail-fast for foundry tests, to exit after first failure - if "forge test" in cmd and not "--fail-fast" in cmd: + if "forge test" in cmd and "--fail-fast" not in cmd: cmd += " --fail-fast" # add --bail for hardhat and truffle tests, to exit after first failure - elif "hardhat test" in cmd or "truffle test" and not "--bail" in cmd: + elif "hardhat test" in cmd or "truffle test" in cmd and "--bail" not in cmd: cmd += " --bail" start = time.time() + # starting new process - P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) + P = subprocess.Popen( + [cmd], + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setsid + ) try: # checking whether the process is completed or not within 30 seconds(default) while P.poll() is None and (time.time() - start) < timeout: @@ -37,30 +51,34 @@ def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: if P.poll() is None: logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") # sends a SIGTERM signal to process group - bascially killing the process - os.killpg(os.getpgid(P.pid), signal.SIGTERM) + os.killpg(os.getpgid(P.pid), signal.SIGTERM) # Avoid any weird race conditions from grabbing the return code time.sleep(0.05) # indicates whether the command executed sucessfully or not r = P.returncode # if r is 0 then it is valid mutant because tests didn't fail - return True if r == 0 else False + return r == 0 -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: - with open(file, 'r') as filepath: +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: # pylint: disable=too-many-arguments + """ + function to verify the validity of each patch + returns: valid or invalid patch + """ + with open(file, 'r', encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] # Write the modified content back to the file - with open(file, 'w') as filepath: + with open(file, 'w', encoding="utf-8") as filepath: filepath.write(replaced_content) - if(compile_generated_mutant(file, mappings)): - if(run_test_cmd(command, file, timeout)): + if compile_generated_mutant(file, mappings): + if run_test_cmd(command, file, timeout): create_mutant_file(file, index, generator_name) - logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) + print(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) return True - + reset_file(file) if verbose: - logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) - return False \ No newline at end of file + print(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) + return False From 89e90b7540dd26c606f4a352b9c57b5b11a1e9d4 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Thu, 25 Jan 2024 19:22:08 -0500 Subject: [PATCH 25/59] Updated with formatting --- slither/tools/mutator/__main__.py | 90 +++++++++++------- slither/tools/mutator/mutators/AOR.py | 19 +++- slither/tools/mutator/mutators/ASOR.py | 29 ++++-- slither/tools/mutator/mutators/BOR.py | 17 +++- slither/tools/mutator/mutators/CR.py | 22 ++++- slither/tools/mutator/mutators/FHR.py | 21 +++-- slither/tools/mutator/mutators/LIR.py | 35 ++++--- slither/tools/mutator/mutators/LOR.py | 15 ++- slither/tools/mutator/mutators/MIA.py | 24 ++++- slither/tools/mutator/mutators/MVIE.py | 11 ++- slither/tools/mutator/mutators/MVIV.py | 7 +- slither/tools/mutator/mutators/MWA.py | 14 ++- slither/tools/mutator/mutators/ROR.py | 20 +++- slither/tools/mutator/mutators/RR.py | 23 ++++- slither/tools/mutator/mutators/SBR.py | 93 ++++++++++++------- slither/tools/mutator/mutators/UOR.py | 58 ++++++++++-- .../mutator/mutators/abstract_mutator.py | 48 +++++++--- .../tools/mutator/mutators/all_mutators.py | 30 +++--- slither/tools/mutator/utils/command_line.py | 3 +- slither/tools/mutator/utils/file_handling.py | 45 ++++++--- slither/tools/mutator/utils/patch.py | 11 ++- .../mutator/utils/testing_generated_mutant.py | 74 +++++++++------ 22 files changed, 504 insertions(+), 205 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 56582d6f54..f0b5c88bce 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -11,7 +11,11 @@ from slither.utils.colors import yellow, magenta from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators -from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list +from .utils.file_handling import ( + transfer_and_delete, + backup_source_file, + get_sol_file_list, +) logging.basicConfig() logger = logging.getLogger("Slither-Mutate") @@ -23,6 +27,7 @@ ################################################################################### ################################################################################### + def parse_args() -> argparse.Namespace: """ Parse the underlying arguments for the program. @@ -33,7 +38,9 @@ def parse_args() -> argparse.Namespace: usage="slither-mutate --test-cmd ", ) - parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)") + parser.add_argument( + "codebase", help="Codebase to analyze (.sol file, project directory, ...)" + ) parser.add_argument( "--list-mutators", @@ -44,33 +51,22 @@ def parse_args() -> argparse.Namespace: ) # argument to add the test command - parser.add_argument( - "--test-cmd", - help="Command to run the tests for your project" - ) + parser.add_argument("--test-cmd", help="Command to run the tests for your project") # argument to add the test directory - containing all the tests - parser.add_argument( - "--test-dir", - help="Tests directory" - ) + parser.add_argument("--test-dir", help="Tests directory") # argument to ignore the interfaces, libraries - parser.add_argument( - "--ignore-dirs", - help="Directories to ignore" - ) + parser.add_argument("--ignore-dirs", help="Directories to ignore") # time out argument parser.add_argument( - "--timeout", - help="Set timeout for test command (by default 30 seconds)" + "--timeout", help="Set timeout for test command (by default 30 seconds)" ) # output directory argument parser.add_argument( - "--output-dir", - help="Name of output directory (by default 'mutation_campaign')" + "--output-dir", help="Name of output directory (by default 'mutation_campaign')" ) # to print just all the mutants @@ -110,14 +106,26 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() + def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] if mutators_list is not None: - detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) and str(c.NAME) in mutators_list ] + detectors = [ + c + for c in detectors_ + if inspect.isclass(c) + and issubclass(c, AbstractMutator) + and str(c.NAME) in mutators_list + ] else: - detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) ] + detectors = [ + c + for c in detectors_ + if inspect.isclass(c) and issubclass(c, AbstractMutator) + ] return detectors + class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def __call__( self, parser: Any, *args: Any, **kwargs: Any @@ -126,6 +134,7 @@ def __call__( output_mutators(checks) parser.exit() + # endregion ################################################################################### ################################################################################### @@ -133,7 +142,10 @@ def __call__( ################################################################################### ################################################################################### -def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too-many-locals + +def main() -> ( + None +): # pylint: disable=too-many-statements,too-many-branches,too-many-locals args = parse_args() # arguments @@ -151,7 +163,7 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- logger.info(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) if paths_to_ignore: - paths_to_ignore_list = paths_to_ignore.strip('][').split(',') + paths_to_ignore_list = paths_to_ignore.strip("][").split(",") logger.info(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) else: paths_to_ignore_list = [] @@ -179,14 +191,14 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- for M in duplicate_list: if M.NAME == "RR": mutators_list.remove(M) - CR_RR_list.insert(0,M) + CR_RR_list.insert(0, M) elif M.NAME == "CR": mutators_list.remove(M) - CR_RR_list.insert(1,M) + CR_RR_list.insert(1, M) mutators_list = CR_RR_list + mutators_list - for filename in sol_file_list: # pylint: disable=too-many-nested-blocks - contract_name = os.path.split(filename)[1].split('.sol')[0] + for filename in sol_file_list: # pylint: disable=too-many-nested-blocks + contract_name = os.path.split(filename)[1].split(".sol")[0] # slither object sl = Slither(filename, **vars(args)) # create a backup files @@ -201,24 +213,34 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - contract_instance = '' + contract_instance = "" for contract in compilation_unit_of_main_file.contracts: if contract_names is not None and contract.name in contract_names: contract_instance = contract elif str(contract.name).lower() == contract_name.lower(): contract_instance = contract - if contract_instance == '': + if contract_instance == "": logger.error("Can't find the contract") else: for M in mutators_list: - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder, dont_mutate_lines) + m = M( + compilation_unit_of_main_file, + int(timeout), + test_command, + test_directory, + contract_instance, + solc_remappings, + verbose, + output_folder, + dont_mutate_lines, + ) (count_valid, count_invalid, lines_list) = m.mutate() v_count += count_valid total_count += count_valid + count_invalid dont_mutate_lines = lines_list if not quick_flag: dont_mutate_lines = [] - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(e) except KeyboardInterrupt: @@ -230,7 +252,13 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- transfer_and_delete(files_dict) # output - logger.info(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) + logger.info( + yellow( + f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n" + ) + ) logger.info(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) + + # endregion diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 00506a1bbb..0bf0fb2a29 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -9,20 +9,23 @@ BinaryType.DIVISION, BinaryType.MULTIPLICATION, BinaryType.SUBTRACTION, - BinaryType.MODULO + BinaryType.MODULO, ] + class AOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "AOR" HELP = "Arithmetic operator replacement" def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: try: ir_expression = node.expression - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except continue for ir in node.irs: if isinstance(ir, Binary) and ir.type in arithmetic_operators: @@ -39,5 +42,13 @@ def _mutate(self) -> Dict: if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 0e7452594f..2ff403b386 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,7 +1,10 @@ from typing import Dict from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator -from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation +from slither.core.expressions.assignment_operation import ( + AssignmentOperationType, + AssignmentOperation, +) assignment_operators = [ AssignmentOperationType.ASSIGN_ADDITION, @@ -14,9 +17,10 @@ AssignmentOperationType.ASSIGN_RIGHT_SHIFT, AssignmentOperationType.ASSIGN_MULTIPLICATION, AssignmentOperationType.ASSIGN_DIVISION, - AssignmentOperationType.ASSIGN_MODULO + AssignmentOperationType.ASSIGN_MODULO, ] + class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ASOR" HELP = "Assignment Operator Replacement" @@ -24,16 +28,21 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: - if isinstance(ir.expression, AssignmentOperation) and ir.expression.type in assignment_operators: + if ( + isinstance(ir.expression, AssignmentOperation) + and ir.expression.type in assignment_operators + ): if ir.expression.type == AssignmentOperationType.ASSIGN: continue alternative_ops = assignment_operators[:] try: alternative_ops.remove(ir.expression.type) - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except continue for op in alternative_ops: if op != ir.expression: @@ -44,5 +53,13 @@ def _mutate(self) -> Dict: if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 9f8ca2a118..a8720a4b63 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -8,9 +8,10 @@ BinaryType.OR, BinaryType.LEFT_SHIFT, BinaryType.RIGHT_SHIFT, - BinaryType.CARET + BinaryType.CARET, ] + class BOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "BOR" HELP = "Bitwise Operator Replacement" @@ -18,7 +19,9 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in bitwise_operators: @@ -33,5 +36,13 @@ def _mutate(self) -> Dict: if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 2960282362..ebf93bf18a 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -6,14 +6,20 @@ class CR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "CR" - HELP = 'Comment Replacement' + HELP = "Comment Replacement" def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): + if node.type not in ( + NodeType.ENTRYPOINT, + NodeType.ENDIF, + NodeType.ENDLOOP, + ): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length @@ -21,5 +27,13 @@ def _mutate(self) -> Dict: line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: new_str = "//" + old_str - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 4560db4da2..028c1916cd 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -7,27 +7,36 @@ function_header_replacements = [ "pure ==> view", "view ==> pure", - "(\s)(external|public|internal) ==> \\1private", - "(\s)(external|public) ==> \\1internal" + "(\\s)(external|public|internal) ==> \\1private", + "(\\s)(external|public) ==> \\1internal", ] + class FHR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "FHR" - HELP = 'Function Header Replacement' + HELP = "Function Header Replacement" def _mutate(self) -> Dict: result: Dict = {} for function in self.contract.functions_and_modifiers_declared: start = function.source_mapping.start - stop = start + function.source_mapping.content.find('{') + stop = start + function.source_mapping.content.find("{") old_str = self.in_file_str[start:stop] line_no = function.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in function_header_replacements: - left_value = value.split(" ==> ")[0] + left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index f9ef874cd9..066a1fbb0e 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -7,16 +7,19 @@ literal_replacements = [] + class LIR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LIR" HELP = "Literal Interger Replacement" - def _mutate(self) -> Dict: # pylint: disable=too-many-branches + def _mutate(self) -> Dict: # pylint: disable=too-many-branches result: Dict = {} variable: Variable # Create fault for state variables declaration - for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + variable + ) in self.contract.state_variables_declared: if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: @@ -24,12 +27,16 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches if isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): - literal_replacements.append(variable.type.min) # append data type min value - literal_replacements.append(variable.type.max) # append data type max value + literal_replacements.append( + variable.type.min + ) # append data type min value + literal_replacements.append( + variable.type.max + ) # append data type max value if str(variable.type).startswith("uint"): - literal_replacements.append('1') + literal_replacements.append("1") elif str(variable.type).startswith("uint"): - literal_replacements.append('-1') + literal_replacements.append("-1") # Get the string start = variable.source_mapping.start stop = start + variable.source_mapping.length @@ -37,7 +44,7 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches line_no = variable.node_initialization.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() + old_value = old_str[old_str.find("=") + 1 :].strip() if old_value != value: new_str = f"{old_str.split('=')[0]}= {value}" create_patch_with_line( @@ -47,26 +54,28 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches stop, old_str, new_str, - line_no[0] + line_no[0], ) - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: if variable.initialized and isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): literal_replacements.append(variable.type.min) literal_replacements.append(variable.type.max) if str(variable.type).startswith("uint"): - literal_replacements.append('1') + literal_replacements.append("1") elif str(variable.type).startswith("uint"): - literal_replacements.append('-1') + literal_replacements.append("-1") start = variable.source_mapping.start stop = start + variable.source_mapping.length old_str = self.in_file_str[start:stop] line_no = variable.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for new_value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() + old_value = old_str[old_str.find("=") + 1 :].strip() if old_value != new_value: new_str = f"{old_str.split('=')[0]}= {new_value}" create_patch_with_line( @@ -76,6 +85,6 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches stop, old_str, new_str, - line_no[0] + line_no[0], ) return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index 21837d81e7..2d1535b1aa 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -8,6 +8,7 @@ BinaryType.ANDAND, ] + class LOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LOR" HELP = "Logical Operator Replacement" @@ -15,7 +16,9 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in logical_operators: @@ -31,5 +34,13 @@ def _mutate(self) -> Dict: if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index a10ce1f1d7..f29569f63e 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -4,6 +4,7 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation + class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' @@ -22,10 +23,25 @@ def _mutate(self) -> Dict: # Replace the expression with true and false for value in ["true", "false"]: new_str = value - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(UnaryOperationType.BANG) + "(" + old_str + ")" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 678b3b7475..4d5f6205d5 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -4,6 +4,7 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line + class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" @@ -34,12 +35,14 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): + if variable.initialized and not isinstance( + variable.expression, Literal + ): # Get the string start = variable.source_mapping.start stop = variable.expression.source_mapping.start @@ -54,6 +57,6 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index c82f132d74..f9e51c5533 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -4,6 +4,7 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line + class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" @@ -34,7 +35,7 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) for function in self.contract.functions_and_modifiers_declared: @@ -53,6 +54,6 @@ def _mutate(self) -> Dict: stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 20447290aa..9682f10caf 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -4,6 +4,7 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation + class MWA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MWA" HELP = '"while" construct around statement' @@ -21,7 +22,14 @@ def _mutate(self) -> Dict: line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(UnaryOperationType.BANG) + "(" + old_str + ")" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index d59e7575a9..9daae0663f 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -12,6 +12,7 @@ BinaryType.NOT_EQUAL, ] + class ROR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ROR" HELP = "Relational Operator Replacement" @@ -19,11 +20,16 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in relational_operators: - if str(ir.variable_left.type) != 'address' and str(ir.variable_right) != 'address': + if ( + str(ir.variable_left.type) != "address" + and str(ir.variable_right) != "address" + ): alternative_ops = relational_operators[:] alternative_ops.remove(ir.type) for op in alternative_ops: @@ -35,5 +41,13 @@ def _mutate(self) -> Dict: if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 6bfd644e05..e285d7a3f4 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -3,23 +3,36 @@ from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator + class RR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "RR" - HELP = 'Revert Replacement' + HELP = "Revert Replacement" def _mutate(self) -> Dict: result: Dict = {} for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): + if node.type not in ( + NodeType.ENTRYPOINT, + NodeType.ENDIF, + NodeType.ENDLOOP, + ): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: - if old_str != 'revert()': - new_str = 'revert()' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if old_str != "revert()": + new_str = "revert()" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index bbdc631943..770b57180d 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -6,29 +6,29 @@ from slither.core.variables.variable import Variable solidity_rules = [ - "abi\.encode\( ==> abi.encodePacked(", - "abi\.encodePacked\( ==> abi.encode(", - "\.call([({]) ==> .delegatecall\\1", - "\.call([({]) ==> .staticcall\\1", - "\.delegatecall([({]) ==> .call\\1", - "\.delegatecall([({]) ==> .staticcall\\1", - "\.staticcall([({]) ==> .delegatecall\\1", - "\.staticcall([({]) ==> .call\\1", + "abi\\.encode\\( ==> abi.encodePacked(", + "abi\\.encodePacked\\( ==> abi.encode(", + "\\.call([({]) ==> .delegatecall\\1", + "\\.call([({]) ==> .staticcall\\1", + "\\.delegatecall([({]) ==> .call\\1", + "\\.delegatecall([({]) ==> .staticcall\\1", + "\\.staticcall([({]) ==> .delegatecall\\1", + "\\.staticcall([({]) ==> .call\\1", "^now$ ==> 0", "block.timestamp ==> 0", "msg.value ==> 0", "msg.value ==> 1", - "(\s)(wei|gwei) ==> \\1ether", - "(\s)(ether|gwei) ==> \\1wei", - "(\s)(wei|ether) ==> \\1gwei", - "(\s)(minutes|days|hours|weeks) ==> \\1seconds", - "(\s)(seconds|days|hours|weeks) ==> \\1minutes", - "(\s)(seconds|minutes|hours|weeks) ==> \\1days", - "(\s)(seconds|minutes|days|weeks) ==> \\1hours", - "(\s)(seconds|minutes|days|hours) ==> \\1weeks", - "(\s)(memory) ==> \\1storage", - "(\s)(storage) ==> \\1memory", - "(\s)(constant) ==> \\1immutable", + "(\\s)(wei|gwei) ==> \\1ether", + "(\\s)(ether|gwei) ==> \\1wei", + "(\\s)(wei|ether) ==> \\1gwei", + "(\\s)(minutes|days|hours|weeks) ==> \\1seconds", + "(\\s)(seconds|days|hours|weeks) ==> \\1minutes", + "(\\s)(seconds|minutes|hours|weeks) ==> \\1days", + "(\\s)(seconds|minutes|days|weeks) ==> \\1hours", + "(\\s)(seconds|minutes|days|hours) ==> \\1weeks", + "(\\s)(memory) ==> \\1storage", + "(\\s)(storage) ==> \\1memory", + "(\\s)(constant) ==> \\1immutable", "addmod ==> mulmod", "mulmod ==> addmod", "msg.sender ==> tx.origin", @@ -40,45 +40,74 @@ "(u?)int64 ==> \\1int32", "(u?)int128 ==> \\1int64", "(u?)int256 ==> \\1int128", - "while ==> if", + "while ==> if", ] + class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" - HELP = 'Solidity Based Replacement' + HELP = "Solidity Based Replacement" def _mutate(self) -> Dict: result: Dict = {} variable: Variable - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): + if node.type not in ( + NodeType.ENTRYPOINT, + NodeType.ENDIF, + NodeType.ENDLOOP, + ): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in solidity_rules: - left_value = value.split(" ==> ")[0] + left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = re.sub( + re.compile(left_value), right_value, old_str + ) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) - for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + variable + ) in self.contract.state_variables_declared: node = variable.node_initialization if node: start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in solidity_rules: - left_value = value.split(" ==> ")[0] + left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = re.sub( + re.compile(left_value), right_value, old_str + ) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index b7e8c81569..f427c2fbf6 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -8,9 +8,10 @@ UnaryOperationType.MINUSMINUS_PRE, UnaryOperationType.PLUSPLUS_POST, UnaryOperationType.MINUSMINUS_POST, - UnaryOperationType.MINUS_PRE + UnaryOperationType.MINUS_PRE, ] + class UOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "UOR" HELP = "Unary Operator Replacement" @@ -18,33 +19,70 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: try: ir_expression = node.expression - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except continue start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: - if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: + if ( + isinstance(ir_expression, UnaryOperation) + and ir_expression.type in unary_operators + ): for op in unary_operators: if not node.expression.is_prefix: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(variable_read) + str(op) - if new_str != old_str and str(op) != '-': - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if new_str != old_str and str(op) != "-": + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) new_str = str(op) + str(variable_read) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) else: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(op) + str(variable_read) - if new_str != old_str and str(op) != '-': - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if new_str != old_str and str(op) != "-": + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) new_str = str(variable_read) + str(op) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index c2f15cd748..12c6b22983 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -9,17 +9,22 @@ logger = logging.getLogger("Slither-Mutate") + class IncorrectMutatorInitialization(Exception): pass -class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods + +class AbstractMutator( + metaclass=abc.ABCMeta +): # pylint: disable=too-few-public-methods,too-many-instance-attributes NAME = "" HELP = "" VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 - def __init__( # pylint: disable=too-many-arguments - self, compilation_unit: SlitherCompilationUnit, + def __init__( # pylint: disable=too-many-arguments + self, + compilation_unit: SlitherCompilationUnit, timeout: int, testing_command: str, testing_directory: str, @@ -29,7 +34,7 @@ def __init__( # pylint: disable=too-many-arguments output_folder: str, dont_mutate_line: List[int], rate: int = 10, - seed: Optional[int] = None + seed: Optional[int] = None, ) -> None: self.compilation_unit = compilation_unit self.slither = compilation_unit.core @@ -71,7 +76,7 @@ def mutate(self) -> Tuple[int, int, List[int]]: (all_patches) = self._mutate() if "patches" not in all_patches: logger.debug("No patches found by %s", self.NAME) - return (0,0,self.dont_mutate_line) + return (0, 0, self.dont_mutate_line) for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") @@ -80,21 +85,38 @@ def mutate(self) -> Tuple[int, int, List[int]]: logger.info(yellow(f"Mutating {file} with {self.NAME} \n")) for patch in patches: # test the patch - flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) + flag = test_patch( + file, + patch, + self.test_command, + self.VALID_MUTANTS_COUNT, + self.NAME, + self.timeout, + self.solc_remappings, + self.verbose, + ) # if RR or CR and valid mutant, add line no. - if self.NAME in ('RR', 'CR') and flag: - self.dont_mutate_line.append(patch['line_number']) + if self.NAME in ("RR", "CR") and flag: + self.dont_mutate_line.append(patch["line_number"]) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 continue self.VALID_MUTANTS_COUNT += 1 - patched_txt,_ = apply_patch(original_txt, patch, 0) - diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) + patched_txt, _ = apply_patch(original_txt, patch, 0) + diff = create_diff( + self.compilation_unit, original_txt, patched_txt, file + ) if not diff: logger.info(f"Impossible to generate patch; empty {patches}") # add valid mutant patches to a output file - with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: - patches_file.write(diff + '\n') - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) + with open( + self.output_folder + "/patches_file.txt", "a", encoding="utf8" + ) as patches_file: + patches_file.write(diff + "\n") + return ( + self.VALID_MUTANTS_COUNT, + self.INVALID_MUTANTS_COUNT, + self.dont_mutate_line, + ) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 852a8efc34..b02a2cc9b9 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,16 +1,16 @@ # pylint: disable=unused-import -from slither.tools.mutator.mutators.MVIV import MVIV # severity low -from slither.tools.mutator.mutators.MVIE import MVIE # severity low -from slither.tools.mutator.mutators.LOR import LOR # severity medium -from slither.tools.mutator.mutators.UOR import UOR # severity medium -from slither.tools.mutator.mutators.SBR import SBR # severity medium -from slither.tools.mutator.mutators.AOR import AOR # severity medium -from slither.tools.mutator.mutators.BOR import BOR # severity medium -from slither.tools.mutator.mutators.ASOR import ASOR # severity medium -from slither.tools.mutator.mutators.MWA import MWA # severity medium -from slither.tools.mutator.mutators.LIR import LIR # severity medium -from slither.tools.mutator.mutators.FHR import FHR # severity medium -from slither.tools.mutator.mutators.MIA import MIA # severity medium -from slither.tools.mutator.mutators.ROR import ROR # severity medium -from slither.tools.mutator.mutators.RR import RR # severity high -from slither.tools.mutator.mutators.CR import CR # severity high +from slither.tools.mutator.mutators.MVIV import MVIV # severity low +from slither.tools.mutator.mutators.MVIE import MVIE # severity low +from slither.tools.mutator.mutators.LOR import LOR # severity medium +from slither.tools.mutator.mutators.UOR import UOR # severity medium +from slither.tools.mutator.mutators.SBR import SBR # severity medium +from slither.tools.mutator.mutators.AOR import AOR # severity medium +from slither.tools.mutator.mutators.BOR import BOR # severity medium +from slither.tools.mutator.mutators.ASOR import ASOR # severity medium +from slither.tools.mutator.mutators.MWA import MWA # severity medium +from slither.tools.mutator.mutators.LIR import LIR # severity medium +from slither.tools.mutator.mutators.FHR import FHR # severity medium +from slither.tools.mutator.mutators.MIA import MIA # severity medium +from slither.tools.mutator.mutators.ROR import ROR # severity medium +from slither.tools.mutator.mutators.RR import RR # severity high +from slither.tools.mutator.mutators.CR import CR # severity high diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index 358586688c..79d7050972 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -2,6 +2,7 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.utils.myprettytable import MyPrettyTable + def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: mutators_list = [] for detector in mutators_classes: @@ -13,7 +14,7 @@ def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: # Sort by class mutators_list = sorted(mutators_list, key=lambda element: (element[0])) idx = 1 - for (argument, help_info) in mutators_list: + for argument, help_info in mutators_list: table.add_row([str(idx), argument, help_info]) idx = idx + 1 print(table) diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index d4474c3101..ddb3efb50a 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -6,6 +6,7 @@ duplicated_files = {} + def backup_source_file(source_code: Dict, output_folder: str) -> Dict: """ function to backup the source file @@ -18,21 +19,22 @@ def backup_source_file(source_code: Dict, output_folder: str) -> Dict: new_filename = f"{output_folder}/backup_{filename}" new_file_path = os.path.join(directory, new_filename) - with open(new_file_path, 'w', encoding="utf8") as new_file: + with open(new_file_path, "w", encoding="utf8") as new_file: new_file.write(content) duplicated_files[file_path] = new_file_path return duplicated_files + def transfer_and_delete(files_dict: Dict) -> None: """function to transfer the original content to the sol file after campaign""" try: files_dict_copy = files_dict.copy() for item, value in files_dict_copy.items(): - with open(value, 'r', encoding="utf8") as duplicated_file: + with open(value, "r", encoding="utf8") as duplicated_file: content = duplicated_file.read() - with open(item, 'w', encoding="utf8") as original_file: + with open(item, "w", encoding="utf8") as original_file: original_file.write(content) os.remove(value) @@ -40,47 +42,61 @@ def transfer_and_delete(files_dict: Dict) -> None: # delete elements from the global dict del duplicated_files[item] - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(f"Error transferring content: {e}") + def create_mutant_file(file: str, count: int, rule: str) -> None: """function to create new mutant file""" try: _, filename = os.path.split(file) # Read content from the duplicated file - with open(file, 'r', encoding="utf8") as source_file: + with open(file, "r", encoding="utf8") as source_file: content = source_file.read() # Write content to the original file - mutant_name = filename.split('.')[0] + mutant_name = filename.split(".")[0] # create folder for each contract os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) - with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w', encoding="utf8") as mutant_file: + with open( + "mutation_campaign/" + + mutant_name + + "/" + + mutant_name + + "_" + + rule + + "_" + + str(count) + + ".sol", + "w", + encoding="utf8", + ) as mutant_file: mutant_file.write(content) # reset the file - with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: + with open(duplicated_files[file], "r", encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w', encoding="utf8") as source_file: + with open(file, "w", encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(f"Error creating mutant: {e}") + def reset_file(file: str) -> None: """function to reset the file""" try: # directory, filename = os.path.split(file) # reset the file - with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: + with open(duplicated_files[file], "r", encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w', encoding="utf8") as source_file: + with open(file, "w", encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(f"Error resetting file: {e}") @@ -92,12 +108,13 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str sol_file_list = [] if ignore_paths is None: ignore_paths = [] + # if input is contract file if os.path.isfile(codebase): return [codebase] # if input is folder - elif os.path.isdir(codebase): + if os.path.isdir(codebase): directory = os.path.abspath(codebase) for file in os.listdir(directory): filename = os.path.join(directory, file) diff --git a/slither/tools/mutator/utils/patch.py b/slither/tools/mutator/utils/patch.py index e50f44612b..54ff81e60b 100644 --- a/slither/tools/mutator/utils/patch.py +++ b/slither/tools/mutator/utils/patch.py @@ -1,6 +1,7 @@ from typing import Dict, Union from collections import defaultdict + # pylint: disable=too-many-arguments def create_patch_with_line( result: Dict, @@ -9,13 +10,19 @@ def create_patch_with_line( end: int, old_str: Union[str, bytes], new_str: Union[str, bytes], - line_no: int + line_no: int, ) -> None: if isinstance(old_str, bytes): old_str = old_str.decode("utf8") if isinstance(new_str, bytes): new_str = new_str.decode("utf8") - p = {"start": start, "end": end, "old_string": old_str, "new_string": new_str, "line_number": line_no} + p = { + "start": start, + "end": end, + "old_string": old_str, + "new_string": new_str, + "line_number": line_no, + } if "patches" not in result: result["patches"] = defaultdict(list) if p not in result["patches"][file]: diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 5c75f0eb31..0e495f8ae6 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -10,6 +10,7 @@ logger = logging.getLogger("Slither-Mutate") + def compile_generated_mutant(file_path: str, mappings: str) -> bool: """ function to compile the generated mutant @@ -21,11 +22,14 @@ def compile_generated_mutant(file_path: str, mappings: str) -> bool: except: # pylint: disable=bare-except return False -def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: + +def run_test_cmd(cmd: str, test_dir: str, timeout: int) -> bool: """ function to run codebase tests returns: boolean whether the tests passed or not """ + # future purpose + _ = test_dir # add --fail-fast for foundry tests, to exit after first failure if "forge test" in cmd and "--fail-fast" not in cmd: cmd += " --fail-fast" @@ -36,49 +40,65 @@ def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: start = time.time() # starting new process - P = subprocess.Popen( - [cmd], - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=os.setsid - ) - try: - # checking whether the process is completed or not within 30 seconds(default) - while P.poll() is None and (time.time() - start) < timeout: - time.sleep(0.05) - finally: - if P.poll() is None: - logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") - # sends a SIGTERM signal to process group - bascially killing the process - os.killpg(os.getpgid(P.pid), signal.SIGTERM) - # Avoid any weird race conditions from grabbing the return code - time.sleep(0.05) - # indicates whether the command executed sucessfully or not - r = P.returncode + with subprocess.Popen( + [cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) as P: + try: + # checking whether the process is completed or not within 30 seconds(default) + while P.poll() is None and (time.time() - start) < timeout: + time.sleep(0.05) + finally: + if P.poll() is None: + logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") + # sends a SIGTERM signal to process group - bascially killing the process + os.killpg(os.getpgid(P.pid), signal.SIGTERM) + # Avoid any weird race conditions from grabbing the return code + time.sleep(0.05) + # indicates whether the command executed sucessfully or not + r = P.returncode # if r is 0 then it is valid mutant because tests didn't fail return r == 0 -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: # pylint: disable=too-many-arguments + +def test_patch( # pylint: disable=too-many-arguments + file: str, + patch: Dict, + command: str, + index: int, + generator_name: str, + timeout: int, + mappings: str | None, + verbose: bool, +) -> bool: """ function to verify the validity of each patch returns: valid or invalid patch """ - with open(file, 'r', encoding="utf-8") as filepath: + with open(file, "r", encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values - replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] + replaced_content = ( + content[: patch["start"]] + patch["new_string"] + content[patch["end"] :] + ) # Write the modified content back to the file - with open(file, 'w', encoding="utf-8") as filepath: + with open(file, "w", encoding="utf-8") as filepath: filepath.write(replaced_content) if compile_generated_mutant(file, mappings): if run_test_cmd(command, file, timeout): create_mutant_file(file, index, generator_name) - print(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) + print( + green( + f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n" + ) + ) return True reset_file(file) if verbose: - print(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) + print( + red( + f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n" + ) + ) return False From 5c88d6e29104d4f6bc8afed793111cdf7f52af48 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Fri, 26 Jan 2024 11:37:05 -0500 Subject: [PATCH 26/59] Formatted mutators --- slither/tools/mutator/__main__.py | 18 ++++-------------- slither/tools/mutator/mutators/LIR.py | 8 ++------ slither/tools/mutator/mutators/MVIE.py | 4 +--- slither/tools/mutator/mutators/SBR.py | 8 ++------ .../tools/mutator/mutators/abstract_mutator.py | 4 +--- .../mutator/utils/testing_generated_mutant.py | 8 ++------ 6 files changed, 12 insertions(+), 38 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index f0b5c88bce..5c13d7aeae 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -38,9 +38,7 @@ def parse_args() -> argparse.Namespace: usage="slither-mutate --test-cmd ", ) - parser.add_argument( - "codebase", help="Codebase to analyze (.sol file, project directory, ...)" - ) + parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)") parser.add_argument( "--list-mutators", @@ -60,9 +58,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--ignore-dirs", help="Directories to ignore") # time out argument - parser.add_argument( - "--timeout", help="Set timeout for test command (by default 30 seconds)" - ) + parser.add_argument("--timeout", help="Set timeout for test command (by default 30 seconds)") # output directory argument parser.add_argument( @@ -118,11 +114,7 @@ def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator] and str(c.NAME) in mutators_list ] else: - detectors = [ - c - for c in detectors_ - if inspect.isclass(c) and issubclass(c, AbstractMutator) - ] + detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator)] return detectors @@ -143,9 +135,7 @@ def __call__( ################################################################################### -def main() -> ( - None -): # pylint: disable=too-many-statements,too-many-branches,too-many-locals +def main() -> (None): # pylint: disable=too-many-statements,too-many-branches,too-many-locals args = parse_args() # arguments diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index 066a1fbb0e..cc58cbae16 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -27,12 +27,8 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches if isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): - literal_replacements.append( - variable.type.min - ) # append data type min value - literal_replacements.append( - variable.type.max - ) # append data type max value + literal_replacements.append(variable.type.min) # append data type min value + 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"): diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 4d5f6205d5..ce51792ffc 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -40,9 +40,7 @@ def _mutate(self) -> Dict: for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: - if variable.initialized and not isinstance( - variable.expression, Literal - ): + if variable.initialized and not isinstance(variable.expression, Literal): # Get the string start = variable.source_mapping.start stop = variable.expression.source_mapping.start diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 770b57180d..efbda48774 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -71,9 +71,7 @@ def _mutate(self) -> Dict: left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub( - re.compile(left_value), right_value, old_str - ) + new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line( result, self.in_file, @@ -98,9 +96,7 @@ def _mutate(self) -> Dict: left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub( - re.compile(left_value), right_value, old_str - ) + new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line( result, self.in_file, diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 12c6b22983..375af1e6fd 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -104,9 +104,7 @@ def mutate(self) -> Tuple[int, int, List[int]]: continue self.VALID_MUTANTS_COUNT += 1 patched_txt, _ = apply_patch(original_txt, patch, 0) - diff = create_diff( - self.compilation_unit, original_txt, patched_txt, file - ) + diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) if not diff: logger.info(f"Impossible to generate patch; empty {patches}") diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 0e495f8ae6..6104cdd8a9 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -40,9 +40,7 @@ def run_test_cmd(cmd: str, test_dir: str, timeout: int) -> bool: start = time.time() # starting new process - with subprocess.Popen( - [cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) as P: + with subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as P: try: # checking whether the process is completed or not within 30 seconds(default) while P.poll() is None and (time.time() - start) < timeout: @@ -78,9 +76,7 @@ def test_patch( # pylint: disable=too-many-arguments with open(file, "r", encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values - replaced_content = ( - content[: patch["start"]] + patch["new_string"] + content[patch["end"] :] - ) + replaced_content = (content[: patch["start"]] + patch["new_string"] + content[patch["end"] :]) # Write the modified content back to the file with open(file, "w", encoding="utf-8") as filepath: filepath.write(replaced_content) From 31ef3f12e30dfe9924958802d96479a3b2157014 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Fri, 26 Jan 2024 12:25:32 -0500 Subject: [PATCH 27/59] Formatted test_patch --- slither/tools/mutator/utils/testing_generated_mutant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 6104cdd8a9..4c51b7e5af 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -76,7 +76,7 @@ def test_patch( # pylint: disable=too-many-arguments with open(file, "r", encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values - replaced_content = (content[: patch["start"]] + patch["new_string"] + content[patch["end"] :]) + replaced_content = content[: patch["start"]] + patch["new_string"] + content[patch["end"] :] # Write the modified content back to the file with open(file, "w", encoding="utf-8") as filepath: filepath.write(replaced_content) From 114e8644f33f348583e49b51f9537bd4b9d0be4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:41:54 -0600 Subject: [PATCH 28/59] Bump cachix/cachix-action from 12 to 14 (#2275) Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 12 to 14. - [Release notes](https://github.com/cachix/cachix-action/releases) - [Commits](https://github.com/cachix/cachix-action/compare/v12...v14) --- updated-dependencies: - dependency-name: cachix/cachix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a646bb42d..9675b0d359 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: - name: Set up cachix if: matrix.type == 'dapp' - uses: cachix/cachix-action@v12 + uses: cachix/cachix-action@v14 with: name: dapp From e944173df0ea6d07cf7234a48ccbade56b0f5bed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:42:07 -0600 Subject: [PATCH 29/59] Bump actions/upload-artifact from 3 to 4 (#2265) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e0ab4b5434..dd63b5f478 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: python -m pip install build python -m build - name: Upload distributions - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: slither-dists path: dist/ From 8e4e634323cbe6e13f3119e814455b8521d33ad5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:42:16 -0600 Subject: [PATCH 30/59] Bump actions/setup-python from 4 to 5 (#2259) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/black.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/doctor.yml | 2 +- .github/workflows/linter.yml | 2 +- .github/workflows/pip-audit.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/pylint.yml | 2 +- .github/workflows/test.yml | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 6017255b26..082333f2fc 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -32,7 +32,7 @@ jobs: fetch-depth: 0 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9675b0d359..dbde094890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install dependencies diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e28c7136a7..3d5eeed65d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.8' - run: pip install -e ".[doc]" diff --git a/.github/workflows/doctor.yml b/.github/workflows/doctor.yml index 0a0eb896de..555452871f 100644 --- a/.github/workflows/doctor.yml +++ b/.github/workflows/doctor.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 5415b6d1be..45dc0d8c28 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -31,7 +31,7 @@ jobs: fetch-depth: 0 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/pip-audit.yml b/.github/workflows/pip-audit.yml index a98f6ab58b..1c0a1d40ad 100644 --- a/.github/workflows/pip-audit.yml +++ b/.github/workflows/pip-audit.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dd63b5f478..7fc17804cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 7e990371ff..091da2b968 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1660a25e9c..68a32f80a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} cache: "pip" @@ -102,7 +102,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 From d0761a39136e58a6fb869371c2606e431d2cc190 Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Mon, 29 Jan 2024 22:42:45 +0100 Subject: [PATCH 31/59] Detect also in modifiers (#2280) --- slither/detectors/statements/divide_before_multiply.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/detectors/statements/divide_before_multiply.py b/slither/detectors/statements/divide_before_multiply.py index 334da592c0..1f6ccd87e1 100644 --- a/slither/detectors/statements/divide_before_multiply.py +++ b/slither/detectors/statements/divide_before_multiply.py @@ -133,7 +133,7 @@ def detect_divide_before_multiply( results: List[Tuple[FunctionContract, List[Node]]] = [] # Loop for each function and modifier. - for function in contract.functions_declared: + for function in contract.functions_declared + contract.modifiers_declared: if not function.entry_point: continue From 04426e0e694723c8e315d2694eb40bb994aeb0ba Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 29 Jan 2024 15:43:14 -0600 Subject: [PATCH 32/59] Fix/iterative update (#2206) * use iterative algo to prevent exceeding recursion limit * proper fixpoint --- slither/solc_parsing/declarations/function.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/slither/solc_parsing/declarations/function.py b/slither/solc_parsing/declarations/function.py index 59940ec1cd..c1b94661d1 100644 --- a/slither/solc_parsing/declarations/function.py +++ b/slither/solc_parsing/declarations/function.py @@ -1106,11 +1106,13 @@ def _parse_unchecked_block(self, block: Dict, node: NodeSolc, scope): return node def _update_reachability(self, node: Node) -> None: - if node.is_reachable: - return - node.set_is_reachable(True) - for son in node.sons: - self._update_reachability(son) + worklist = [node] + while worklist: + current = worklist.pop() + # fix point + if not current.is_reachable: + current.set_is_reachable(True) + worklist.extend(current.sons) def _parse_cfg(self, cfg: Dict) -> None: From cda9a9dc207d48121b2752bf857f2bc4e2c7d9bf Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 29 Jan 2024 15:44:25 -0600 Subject: [PATCH 33/59] fix: detect selfdestruct in internal calls (#2232) * fix: detect selfdestruct in internal calls * update snapshot --- slither/detectors/functions/suicidal.py | 2 +- ...etector_Suicidal_0_7_6_suicidal_sol__0.txt | 2 ++ .../test_data/suicidal/0.7.6/suicidal.sol | 8 ++++++++ .../suicidal/0.7.6/suicidal.sol-0.7.6.zip | Bin 1478 -> 1918 bytes 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/slither/detectors/functions/suicidal.py b/slither/detectors/functions/suicidal.py index 1f8cb52f9c..f0af978ec7 100644 --- a/slither/detectors/functions/suicidal.py +++ b/slither/detectors/functions/suicidal.py @@ -59,7 +59,7 @@ def detect_suicidal_func(func: FunctionContract) -> bool: if func.visibility not in ["public", "external"]: return False - calls = [c.name for c in func.internal_calls] + calls = [c.name for c in func.all_internal_calls()] if not ("suicide(address)" in calls or "selfdestruct(address)" in calls): return False diff --git a/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt index 4a784217df..99a6a0295f 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt @@ -1,2 +1,4 @@ +C.i_am_a_backdoor2(address) (tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol#8-10) allows anyone to destruct the contract + C.i_am_a_backdoor() (tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol#4-6) allows anyone to destruct the contract diff --git a/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol b/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol index 428c794d4d..31b22d767a 100644 --- a/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol +++ b/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol @@ -5,4 +5,12 @@ contract C{ selfdestruct(msg.sender); } + function i_am_a_backdoor2(address payable to) public{ + internal_selfdestruct(to); + } + + function internal_selfdestruct(address payable to) internal { + selfdestruct(to); + } + } diff --git a/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol-0.7.6.zip b/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol-0.7.6.zip index 635092d49ecdda4ce21c4da09b8ba5b061e9a72e..ecd80364b245bf08dcb196de7486ec55a28df991 100644 GIT binary patch delta 1676 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 Date: Tue, 30 Jan 2024 08:36:21 -0600 Subject: [PATCH 34/59] Bump actions/deploy-pages from 3 to 4 (#2285) Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 3 to 4. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3d5eeed65d..4292d5f444 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,4 +43,4 @@ jobs: path: './html/' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v3 + uses: actions/deploy-pages@v4 From 9c2110e9371ec2e890868fc0857c7953fdfe0c1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 08:36:32 -0600 Subject: [PATCH 35/59] Bump cachix/install-nix-action from 24 to 25 (#2286) Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 24 to 25. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v24...v25) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbde094890..7972c96e77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@v24 + uses: cachix/install-nix-action@v25 - name: Set up cachix if: matrix.type == 'dapp' From 2e089b62a13c82ba34f8e2374b3b13879893be31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 07:46:35 -0600 Subject: [PATCH 36/59] Bump sigstore/gh-action-sigstore-python from 2.1.0 to 2.1.1 (#2293) Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases) - [Commits](https://github.com/sigstore/gh-action-sigstore-python/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: sigstore/gh-action-sigstore-python dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7fc17804cb..72b002d1e7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,7 +47,7 @@ jobs: uses: pypa/gh-action-pypi-publish@v1.8.11 - name: sign - uses: sigstore/gh-action-sigstore-python@v2.1.0 + uses: sigstore/gh-action-sigstore-python@v2.1.1 with: inputs: ./dist/*.tar.gz ./dist/*.whl release-signing-artifacts: true From 565fa7c01bfd27c16df891d21d77e970734eb65b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 07:50:04 -0600 Subject: [PATCH 37/59] Bump actions/upload-pages-artifact from 2 to 3 (#2294) Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4292d5f444..0942afb6dd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,7 +37,7 @@ jobs: - run: pip install -e ".[doc]" - run: pdoc -o html/ slither '!slither.tools' #TODO fix import errors on pdoc run - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: # Upload the doc path: './html/' From 0ded68dae8c7d0bb1a57ddcbd7495adc77e2fcce Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:54:04 +0100 Subject: [PATCH 38/59] Fix using for when used with "this" (#2224) --- slither/slithir/convert.py | 23 ++++++++++++++++++ tests/e2e/solc_parsing/test_ast_parsing.py | 1 + ...g-for-this-contract.sol-0.8.15-compact.zip | Bin 0 -> 2745 bytes ...-for-this-contract.sol-0.8.15-compact.json | 8 ++++++ .../test_data/using-for-this-contract.sol | 13 ++++++++++ 5 files changed, 45 insertions(+) create mode 100644 tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip create mode 100644 tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json create mode 100644 tests/e2e/solc_parsing/test_data/using-for-this-contract.sol diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 4411e3505c..170df8cba9 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -630,6 +630,17 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo if new_ir: return new_ir + # convert library function when used with "this" + if ( + isinstance(t, ElementaryType) + and t.name == "address" + and ir.destination.name == "this" + and UserDefinedType(node_function.contract) in using_for + ): + new_ir = convert_to_library_or_top_level(ir, node, using_for) + if new_ir: + return new_ir + if isinstance(t, UserDefinedType): # UserdefinedType t_type = t.type @@ -1564,6 +1575,18 @@ def convert_to_library_or_top_level( if new_ir: return new_ir + if ( + isinstance(t, ElementaryType) + and t.name == "address" + and ir.destination.name == "this" + and UserDefinedType(node.function.contract) in using_for + ): + new_ir = look_for_library_or_top_level( + contract, ir, using_for, UserDefinedType(node.function.contract) + ) + if new_ir: + return new_ir + return None diff --git a/tests/e2e/solc_parsing/test_ast_parsing.py b/tests/e2e/solc_parsing/test_ast_parsing.py index bc57dc51b5..e233fa993b 100644 --- a/tests/e2e/solc_parsing/test_ast_parsing.py +++ b/tests/e2e/solc_parsing/test_ast_parsing.py @@ -448,6 +448,7 @@ def make_version(minor: int, patch_min: int, patch_max: int) -> List[str]: Test("using-for-functions-list-3-0.8.0.sol", ["0.8.15"]), Test("using-for-functions-list-4-0.8.0.sol", ["0.8.15"]), Test("using-for-global-0.8.0.sol", ["0.8.15"]), + Test("using-for-this-contract.sol", ["0.8.15"]), Test("library_event-0.8.16.sol", ["0.8.16"]), Test("top-level-struct-0.8.0.sol", ["0.8.0"]), Test("yul-top-level-0.8.0.sol", ["0.8.0"]), diff --git a/tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip b/tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip new file mode 100644 index 0000000000000000000000000000000000000000..6950666a479e8b3c138633675a65c8f101cd0069 GIT binary patch literal 2745 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json b/tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json new file mode 100644 index 0000000000..43eca2c9a9 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json @@ -0,0 +1,8 @@ +{ + "Lib": { + "f(Hello)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n" + }, + "Hello": { + "test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + } +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/using-for-this-contract.sol b/tests/e2e/solc_parsing/test_data/using-for-this-contract.sol new file mode 100644 index 0000000000..33bbc74cdc --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/using-for-this-contract.sol @@ -0,0 +1,13 @@ +library Lib { + function f(Hello h) external { + + } +} +contract Hello { + using Lib for Hello; + + function test() external { + this.f(); + } +} + From 64495297cd1130082a8b6ccfa073a6f94f0bbf25 Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Thu, 8 Feb 2024 13:27:50 -0800 Subject: [PATCH 39/59] fix: broken doc links (#2299) * fix: broken doc links * style: black --- README.md | 6 +++--- slither/detectors/assembly/incorrect_return.py | 4 +++- slither/detectors/assembly/return_instead_of_leave.py | 2 +- tests/e2e/compilation/test_resolution.py | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 37f678d7a1..769ac58aa5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Slither - Read the Docs](https://img.shields.io/badge/Slither-Read_the_Docs-2ea44f)](https://crytic.github.io/slither/slither.html) [![Slither - Wiki](https://img.shields.io/badge/Slither-Wiki-2ea44f)](https://github.com/crytic/slither/wiki/SlithIR) -> Join the Empire Hacking Slack +> Join the Empire Hacking Slack > > [![Slack Status](https://slack.empirehacking.nyc/badge.svg)](https://slack.empirehacking.nyc/) > > - Discussions and Support @@ -131,10 +131,10 @@ Num | Detector | What it Detects | Impact | Confidence 20 | `controlled-delegatecall` | [Controlled delegatecall destination](https://github.com/crytic/slither/wiki/Detector-Documentation#controlled-delegatecall) | High | Medium 21 | `delegatecall-loop` | [Payable functions using `delegatecall` inside a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#payable-functions-using-delegatecall-inside-a-loop) | High | Medium 22 | `incorrect-exp` | [Incorrect exponentiation](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation) | High | Medium -23 | `incorrect-return` | [If a `return` is incorrectly used in assembly mode.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return) | High | Medium +23 | `incorrect-return` | [If a `return` is incorrectly used in assembly mode.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-return-in-assembly) | High | Medium 24 | `msg-value-loop` | [msg.value inside a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#msgvalue-inside-a-loop) | High | Medium 25 | `reentrancy-eth` | [Reentrancy vulnerabilities (theft of ethers)](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities) | High | Medium -26 | `return-leave` | [If a `return` is used instead of a `leave`.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return) | High | Medium +26 | `return-leave` | [If a `return` is used instead of a `leave`.](https://github.com/crytic/slither/wiki/Detector-Documentation#return-instead-of-leave-in-assembly) | High | Medium 27 | `storage-array` | [Signed storage integer array compiler bug](https://github.com/crytic/slither/wiki/Detector-Documentation#storage-signed-integer-array) | High | Medium 28 | `unchecked-transfer` | [Unchecked tokens transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-transfer) | High | Medium 29 | `weak-prng` | [Weak PRNG](https://github.com/crytic/slither/wiki/Detector-Documentation#weak-PRNG) | High | Medium diff --git a/slither/detectors/assembly/incorrect_return.py b/slither/detectors/assembly/incorrect_return.py index f5f0a98d9d..bd5a6d8449 100644 --- a/slither/detectors/assembly/incorrect_return.py +++ b/slither/detectors/assembly/incorrect_return.py @@ -39,7 +39,9 @@ class IncorrectReturn(AbstractDetector): IMPACT = DetectorClassification.HIGH CONFIDENCE = DetectorClassification.MEDIUM - WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" + WIKI = ( + "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-return-in-assembly" + ) WIKI_TITLE = "Incorrect return in assembly" WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution." diff --git a/slither/detectors/assembly/return_instead_of_leave.py b/slither/detectors/assembly/return_instead_of_leave.py index a1591d834b..a1ad9c87e9 100644 --- a/slither/detectors/assembly/return_instead_of_leave.py +++ b/slither/detectors/assembly/return_instead_of_leave.py @@ -20,7 +20,7 @@ class ReturnInsteadOfLeave(AbstractDetector): IMPACT = DetectorClassification.HIGH CONFIDENCE = DetectorClassification.MEDIUM - WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-instead-of-leave-in-assembly" WIKI_TITLE = "Return instead of leave in assembly" WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used." diff --git a/tests/e2e/compilation/test_resolution.py b/tests/e2e/compilation/test_resolution.py index af7cbe2c77..c3290624be 100644 --- a/tests/e2e/compilation/test_resolution.py +++ b/tests/e2e/compilation/test_resolution.py @@ -57,6 +57,6 @@ def test_contract_function_parameter(solc_binary_path) -> None: function = contract.functions[0] parameters = function.parameters - assert (parameters[0].name == 'param1') - assert (parameters[1].name == '') - assert (parameters[2].name == 'param3') + assert parameters[0].name == "param1" + assert parameters[1].name == "" + assert parameters[2].name == "param3" From 2450fbc79d463292f5908d17cf64b9492bce5ad8 Mon Sep 17 00:00:00 2001 From: Talfao Date: Tue, 12 Mar 2024 17:25:47 +0100 Subject: [PATCH 40/59] feat: First version of Uniswap spot price detection --- slither/detectors/all_detectors.py | 1 + slither/detectors/oracles/spot_price.py | 138 ++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 slither/detectors/oracles/spot_price.py diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index fab9562d20..4fdf858ed9 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,3 +97,4 @@ from .operations.incorrect_exp import IncorrectOperatorExponentiation from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb +from .oracles.spot_price import SpotPriceDetector \ No newline at end of file diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py new file mode 100644 index 0000000000..55cd24a8f3 --- /dev/null +++ b/slither/detectors/oracles/spot_price.py @@ -0,0 +1,138 @@ +from typing import List +from slither.analyses.data_dependency.data_dependency import get_dependencies +from slither.core.declarations.contract import Contract +from slither.core.declarations.function_contract import FunctionContract +from slither.core.variables.state_variable import StateVariable +from slither.detectors.abstract_detector import AbstractDetector +from slither.slithir.operations import HighLevelCall, InternalCall, Operation, Unpack +from slither.slithir.variables.variable import Variable +from slither.core.cfg.node import Node +from slither.detectors.abstract_detector import DetectorClassification + + +class SpotPriceUsage(): + def __init__(self, node: Node, interface: str): + self.node = node + self.interface = interface + + +class SpotPriceDetector(AbstractDetector): + """ + Documentation + """ + + ARGUMENT = ( + "oracle-spot-price" # slither will launch the detector with slither.py --detect mydetector + ) + HELP = "Oracle vulnerabilities" + IMPACT = DetectorClassification.INFORMATIONAL + CONFIDENCE = DetectorClassification.INFORMATIONAL + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#oracle-sequencer" + + WIKI_TITLE = "Oracle Sequencer" + WIKI_DESCRIPTION = "Detection of oracle sequencer." + WIKI_RECOMMENDATION = "If you deploy contracts on the second layer as Arbitrum, you should perform an additional check if the sequencer is active. For more information visit https://docs.chain.link/data-feeds/l2-sequencer-feeds#available-networks" + + + def detect_uniswap_v3(self, function: FunctionContract) -> Node: + interface = "IUniswapV3Pool" + function_call = "slot0" + return self.wanted_intersting_node_call(function, function_call, interface) + def detect_uniswap_v2(self, function: FunctionContract) -> Node: + interface = "IUniswapV2Pair" + function_call = "getReserves" + return self.wanted_intersting_node_call(function, function_call, interface) + + def instance_of_call(self, ir: Operation, function_name, interface_name) -> bool: + if isinstance(ir, HighLevelCall): + if isinstance(ir.destination, Variable): + if function_name is not None and interface_name is not None: + if str(ir.destination.type) == interface_name and ir.function.name == function_name: + return True + elif function_name is None: + if ir.destination.type == interface_name: + return True + elif interface_name is None: + if ir.function.name == function_name: + return True + + return False + + # def balance_of_usage(self, node_of_pair, ir: Operation) -> bool: + # if isinstance(ir, HighLevelCall): + # if "ERC20" in ir.destination and ir.function.name == "balanceOf": + # if (ir.arguments[0] == node_of_pair): + # return True + # return False + + # def uniswap_v2_pair(self, node: Node) -> bool: + # print(node) + def wanted_intersting_node_call(self, function: FunctionContract, function_name, interface_name) -> Node: + for node in function.nodes: + for ir in node.irs: + if self.instance_of_call(ir, function_name, interface_name): + return node + return None + + + + def detect_any_fork_of_uniswap(self,function: FunctionContract, found_nodes) -> (Node, str): + node = self.wanted_intersting_node_call(function, "getReserves", None) + if node is None: + node = self.wanted_intersting_node_call(function, "slot0", None) + + for n in found_nodes: + if (n.node == node): + return None, None + return node, "Fork" + + + + def detect_spot_price_usage(self): + spot_price_usage = [] + for contract in self.contracts: + for function in contract.functions: + node_uniswap_v3 = SpotPriceUsage(self.detect_uniswap_v3(function), "IUniswapV3Pool") + if node_uniswap_v3.node is not None: + spot_price_usage.append(node_uniswap_v3) + + node_uniswap_v2 = SpotPriceUsage(self.detect_uniswap_v2(function), "IUniswapV2Pair") + if node_uniswap_v2.node is not None: + spot_price_usage.append(node_uniswap_v2) + + node_fork = self.detect_any_fork_of_uniswap(function, spot_price_usage) + node_fork = SpotPriceUsage(node_fork[0], node_fork[1]) + + + if node_fork.node is not None: + spot_price_usage.append(node_fork) + return spot_price_usage + + def generate_informative_messages(self, spot_price_classes): + messages = [] + for spot_price in spot_price_classes: + print(spot_price.node) + print(spot_price.interface) + if spot_price.interface == "IUniswapV3Pool": + messages.append( + f"Spot price usage detected in Uniswap V3 at {spot_price.node.source_mapping}\n" + ) + elif spot_price.interface == "IUniswapV2Pair": + messages.append( + f"Spot price usage detected in Uniswap V2 at {spot_price.node.source_mapping}\n" + ) + else: + messages.append( + f"Spot price usage detected in Uniswap Fork at {spot_price.node.source_mapping}\n" + ) + return messages + + def _detect(self): + results = [] + spot_price_usage = self.detect_spot_price_usage() + if spot_price_usage: + messages = self.generate_informative_messages(spot_price_usage) + res = self.generate_result(messages) + results.append(res) + return results From 20843dff05414eab25e4571b09a7295b88a5239b Mon Sep 17 00:00:00 2001 From: Talfao Date: Tue, 12 Mar 2024 17:26:20 +0100 Subject: [PATCH 41/59] feat: Test data with getReserves --- .../0.6.12/spot_price_getReserves.sol | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol diff --git a/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol b/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol new file mode 100644 index 0000000000..938a4472c5 --- /dev/null +++ b/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol @@ -0,0 +1,220 @@ +// Resource: https://solodit.xyz/issues/m-10-yaxisvotepowerbalanceof-can-be-manipulated-code4rena-yaxis-yaxis-contest-git + +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +library SafeMath { + /** + * @dev Multiplies two numbers, throws on overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256 c) { + if (a == 0) { + return 0; + } + c = a * b; + assert(c / a == b); + return c; + } + + /** + * @dev Integer division of two numbers, truncating the quotient. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + // assert(b > 0); // Solidity automatically throws when dividing by 0 + // uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + return a / b; + } + + /** + * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + assert(b <= a); + return a - b; + } + + /** + * @dev Adds two numbers, throws on overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256 c) { + c = a + b; + assert(c >= a); + return c; + } +} + +interface IRewards { + function balanceOf(address) external view returns (uint256); + function earned(address) external view returns (uint256); + function totalSupply() external view returns (uint256); +} +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance( + address owner, + address spender + ) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom( + address from, + address to, + uint value + ) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit( + address owner, + address spender, + uint value, + uint deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn( + address indexed sender, + uint amount0, + uint amount1, + address indexed to + ); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() + external + view + returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap( + uint amount0Out, + uint amount1Out, + address to, + bytes calldata data + ) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} + +interface IVoteProxy { + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint256); + function balanceOf(address _voter) external view returns (uint256); +} + +interface IERC20 { + event Transfer(address indexed from, address indexed to, uint256 value); + + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 value) external returns (bool); + + function allowance( + address owner, + address spender + ) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); +} + +contract YaxisVotePower is IVoteProxy { + using SafeMath for uint256; + + // solhint-disable-next-line const-name-snakecase + uint8 public constant override decimals = uint8(18); + + IUniswapV2Pair public immutable yaxisEthUniswapV2Pair; + IERC20 public immutable yaxis; + IRewards public immutable rewardsYaxis; + IRewards public immutable rewardsYaxisEth; + + constructor( + address _yaxis, + address _rewardsYaxis, + address _rewardsYaxisEth, + address _yaxisEthUniswapV2Pair + ) public { + yaxis = IERC20(_yaxis); + rewardsYaxis = IRewards(_rewardsYaxis); + rewardsYaxisEth = IRewards(_rewardsYaxisEth); + yaxisEthUniswapV2Pair = IUniswapV2Pair(_yaxisEthUniswapV2Pair); + } + + function totalSupply() external view override returns (uint256) { + return sqrt(yaxis.totalSupply()); + } + + function balanceOf( + address _voter + ) external view override returns (uint256 _balance) { + uint256 _stakeAmount = rewardsYaxisEth.balanceOf(_voter); + (uint256 _yaxReserves, , ) = yaxisEthUniswapV2Pair.getReserves(); + uint256 _supply = yaxisEthUniswapV2Pair.totalSupply(); + _supply = _supply == 0 ? 1e18 : _supply; + uint256 _lpStakingYax = _yaxReserves.mul(_stakeAmount).div(_supply).add( + rewardsYaxisEth.earned(_voter) + ); + uint256 _rewardsYaxisAmount = rewardsYaxis.balanceOf(_voter).add( + rewardsYaxis.earned(_voter) + ); + _balance = sqrt( + yaxis.balanceOf(_voter).add(_lpStakingYax).add(_rewardsYaxisAmount) + ); + } + + function sqrt(uint256 x) private pure returns (uint256 y) { + uint256 z = (x + 1) / 2; + y = x; + while (z < y) { + y = z; + z = (x / z + z) / 2; + } + y = y * (10 ** 9); + } +} From ee7055bab55b93808f42777ee1fe8b9e0c4a2f7c Mon Sep 17 00:00:00 2001 From: Talfao Date: Tue, 12 Mar 2024 20:58:01 +0100 Subject: [PATCH 42/59] feat: balanceOF spot price detection --- slither/detectors/oracles/spot_price.py | 84 ++++++++++++++++--- .../0.6.12/spot_price_getReserves.sol | 4 +- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 55cd24a8f3..941d6c28b0 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -8,6 +8,11 @@ from slither.slithir.variables.variable import Variable from slither.core.cfg.node import Node from slither.detectors.abstract_detector import DetectorClassification +from slither.slithir.operations import ( + Binary, + BinaryType, +) + class SpotPriceUsage(): @@ -30,8 +35,8 @@ class SpotPriceDetector(AbstractDetector): WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#oracle-sequencer" - WIKI_TITLE = "Oracle Sequencer" - WIKI_DESCRIPTION = "Detection of oracle sequencer." + WIKI_TITLE = "Oracle Spot prices" + WIKI_DESCRIPTION = "Detection of spot price usage" WIKI_RECOMMENDATION = "If you deploy contracts on the second layer as Arbitrum, you should perform an additional check if the sequencer is active. For more information visit https://docs.chain.link/data-feeds/l2-sequencer-feeds#available-networks" @@ -56,18 +61,32 @@ def instance_of_call(self, ir: Operation, function_name, interface_name) -> bool elif interface_name is None: if ir.function.name == function_name: return True + return False + + def get_argument_of_high_level_call(self, ir: Operation) -> List[Variable]: + if isinstance(ir, HighLevelCall): + return ir.arguments + return [] - # def balance_of_usage(self, node_of_pair, ir: Operation) -> bool: - # if isinstance(ir, HighLevelCall): - # if "ERC20" in ir.destination and ir.function.name == "balanceOf": - # if (ir.arguments[0] == node_of_pair): - # return True - # return False + def balance_of_spot_price(self, function) -> bool: + print(function) + first_node = None + second_node = None + first_arguments = [] + for node in function.nodes: + for ir in node.irs: + if self.instance_of_call(ir, "balanceOf", None): + print("Here") + arguments = self.get_argument_of_high_level_call(ir) + if first_node is not None and arguments[0] == first_arguments[0]: + second_node = node + return first_node, second_node + first_arguments = arguments + first_node = node + return first_node, second_node - # def uniswap_v2_pair(self, node: Node) -> bool: - # print(node) def wanted_intersting_node_call(self, function: FunctionContract, function_name, interface_name) -> Node: for node in function.nodes: for ir in node.irs: @@ -107,13 +126,39 @@ def detect_spot_price_usage(self): if node_fork.node is not None: spot_price_usage.append(node_fork) + + node1, node2 = self.balance_of_spot_price(function) + if node1 is not None and node2 is not None: + spot_price_usage.append(SpotPriceUsage([node1,node2], "BalanceOF")) + return spot_price_usage + + def detect_arithmetic_operations(self, node: Node) -> bool: + for ir in node.irs: + if isinstance(ir, Binary): + if ir.type in (BinaryType.ADDITION, BinaryType.SUBTRACTION, BinaryType.MULTIPLICATION, BinaryType.DIVISION): + return True + return False + + def are_calculations_made_with_spot_data(self, node: Node) -> bool: + if not isinstance(node, list): + node = [node] + + while node: + variables = node[0].variables_written + for n in node[0].function.nodes: + if n == node[0]: + continue + for var in variables: + if var in n.variables_read: + if self.detect_arithmetic_operations(n): + return True, n + node.pop() + return False, None def generate_informative_messages(self, spot_price_classes): messages = [] for spot_price in spot_price_classes: - print(spot_price.node) - print(spot_price.interface) if spot_price.interface == "IUniswapV3Pool": messages.append( f"Spot price usage detected in Uniswap V3 at {spot_price.node.source_mapping}\n" @@ -122,17 +167,30 @@ def generate_informative_messages(self, spot_price_classes): messages.append( f"Spot price usage detected in Uniswap V2 at {spot_price.node.source_mapping}\n" ) - else: + elif spot_price.interface == "Fork": messages.append( f"Spot price usage detected in Uniswap Fork at {spot_price.node.source_mapping}\n" ) + elif spot_price.interface == "BalanceOF": + messages.append( + f"Spot price usage detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}. It seems like trying to obtain data through balanceOf method.\n" + ) return messages + + def generate_calc_messages(self, node): + return f"Calculations are made with spot price data in {node.source_mapping}\n" def _detect(self): results = [] spot_price_usage = self.detect_spot_price_usage() if spot_price_usage: messages = self.generate_informative_messages(spot_price_usage) + + for spot_price in spot_price_usage: + true, node = self.are_calculations_made_with_spot_data(spot_price.node) + if node is not None: + self.IMPACT = DetectorClassification.HIGH + messages.append(self.generate_calc_messages(node)) res = self.generate_result(messages) results.append(res) return results diff --git a/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol b/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol index 938a4472c5..fce6d4410b 100644 --- a/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol +++ b/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol @@ -197,9 +197,7 @@ contract YaxisVotePower is IVoteProxy { (uint256 _yaxReserves, , ) = yaxisEthUniswapV2Pair.getReserves(); uint256 _supply = yaxisEthUniswapV2Pair.totalSupply(); _supply = _supply == 0 ? 1e18 : _supply; - uint256 _lpStakingYax = _yaxReserves.mul(_stakeAmount).div(_supply).add( - rewardsYaxisEth.earned(_voter) - ); + uint256 _lpStakingYax = (_yaxReserves * _stakeAmount) / _supply; uint256 _rewardsYaxisAmount = rewardsYaxis.balanceOf(_voter).add( rewardsYaxis.earned(_voter) ); From fac7b33de2a139e4d5e03f1d9155b33cd1a0cc37 Mon Sep 17 00:00:00 2001 From: Talfao Date: Fri, 29 Mar 2024 11:25:44 +0100 Subject: [PATCH 43/59] feat: Add comments --- slither/detectors/oracles/spot_price.py | 72 ++++++++++--------- .../0.6.12/spot_price_getReserves.sol | 0 .../0.6.12/spot_price_slot0.sol | 0 .../0.8.20/spot_price_balanceOf.sol | 43 +++++++++++ 4 files changed, 83 insertions(+), 32 deletions(-) rename tests/e2e/detectors/test_data/{spot_price => oracle-spot-price}/0.6.12/spot_price_getReserves.sol (100%) create mode 100644 tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_slot0.sol create mode 100644 tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 941d6c28b0..1f9df4a3fa 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -14,8 +14,8 @@ ) - -class SpotPriceUsage(): +# SpotPriceUsage class to store the node and interface +class SpotPriceUsage: def __init__(self, node: Node, interface: str): self.node = node self.interface = interface @@ -33,52 +33,53 @@ class SpotPriceDetector(AbstractDetector): IMPACT = DetectorClassification.INFORMATIONAL CONFIDENCE = DetectorClassification.INFORMATIONAL - WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#oracle-sequencer" + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#oracle-spot-price" WIKI_TITLE = "Oracle Spot prices" WIKI_DESCRIPTION = "Detection of spot price usage" - WIKI_RECOMMENDATION = "If you deploy contracts on the second layer as Arbitrum, you should perform an additional check if the sequencer is active. For more information visit https://docs.chain.link/data-feeds/l2-sequencer-feeds#available-networks" + WIKI_RECOMMENDATION = "Using spot price for calculations can lead to vulnerabilities. Make sure to validate the data before using it or consider use of TWAP oracles." - def detect_uniswap_v3(self, function: FunctionContract) -> Node: + def detect_uniswap(self, function: FunctionContract, function_call, interface) -> Node: interface = "IUniswapV3Pool" function_call = "slot0" return self.wanted_intersting_node_call(function, function_call, interface) + def detect_uniswap_v2(self, function: FunctionContract) -> Node: interface = "IUniswapV2Pair" function_call = "getReserves" return self.wanted_intersting_node_call(function, function_call, interface) - - def instance_of_call(self, ir: Operation, function_name, interface_name) -> bool: + + def instance_of_call(self, ir: Operation, function_name, interface_name) -> bool: if isinstance(ir, HighLevelCall): if isinstance(ir.destination, Variable): if function_name is not None and interface_name is not None: - if str(ir.destination.type) == interface_name and ir.function.name == function_name: + if ( + str(ir.destination.type) == interface_name + and ir.function.name == function_name + ): return True elif function_name is None: - if ir.destination.type == interface_name: + if str(ir.destination.type) == interface_name: return True elif interface_name is None: if ir.function.name == function_name: return True - return False def get_argument_of_high_level_call(self, ir: Operation) -> List[Variable]: if isinstance(ir, HighLevelCall): return ir.arguments return [] - + def balance_of_spot_price(self, function) -> bool: - print(function) first_node = None second_node = None first_arguments = [] for node in function.nodes: for ir in node.irs: if self.instance_of_call(ir, "balanceOf", None): - print("Here") arguments = self.get_argument_of_high_level_call(ir) if first_node is not None and arguments[0] == first_arguments[0]: second_node = node @@ -86,28 +87,31 @@ def balance_of_spot_price(self, function) -> bool: first_arguments = arguments first_node = node return first_node, second_node - - def wanted_intersting_node_call(self, function: FunctionContract, function_name, interface_name) -> Node: + + def wanted_intersting_node_call( + self, function: FunctionContract, function_name, interface_name + ) -> Node: for node in function.nodes: for ir in node.irs: if self.instance_of_call(ir, function_name, interface_name): return node return None - - - def detect_any_fork_of_uniswap(self,function: FunctionContract, found_nodes) -> (Node, str): + def detect_any_fork_of_uniswap(self, function: FunctionContract, found_nodes) -> (Node, str): node = self.wanted_intersting_node_call(function, "getReserves", None) if node is None: node = self.wanted_intersting_node_call(function, "slot0", None) for n in found_nodes: - if (n.node == node): + if n.node == node: return None, None return node, "Fork" - - + # Detect spot price usage + # 1. Detect Uniswap V3 + # 2. Detect Uniswap V2 + # 3. Detect any fork of Uniswap + # 4. Detect balanceOf method usage which can indicate spot price usage in certain cases def detect_spot_price_usage(self): spot_price_usage = [] for contract in self.contracts: @@ -123,28 +127,32 @@ def detect_spot_price_usage(self): node_fork = self.detect_any_fork_of_uniswap(function, spot_price_usage) node_fork = SpotPriceUsage(node_fork[0], node_fork[1]) - if node_fork.node is not None: spot_price_usage.append(node_fork) node1, node2 = self.balance_of_spot_price(function) if node1 is not None and node2 is not None: - spot_price_usage.append(SpotPriceUsage([node1,node2], "BalanceOF")) + spot_price_usage.append(SpotPriceUsage([node1, node2], "BalanceOF")) return spot_price_usage - + def detect_arithmetic_operations(self, node: Node) -> bool: for ir in node.irs: if isinstance(ir, Binary): - if ir.type in (BinaryType.ADDITION, BinaryType.SUBTRACTION, BinaryType.MULTIPLICATION, BinaryType.DIVISION): + if ir.type in ( + BinaryType.ADDITION, + BinaryType.SUBTRACTION, + BinaryType.MULTIPLICATION, + BinaryType.DIVISION, + ): return True return False - def are_calculations_made_with_spot_data(self, node: Node) -> bool: + def are_calculations_made_with_spot_data(self, node: Node) -> Node: if not isinstance(node, list): node = [node] - while node: + while node: variables = node[0].variables_written for n in node[0].function.nodes: if n == node[0]: @@ -152,10 +160,10 @@ def are_calculations_made_with_spot_data(self, node: Node) -> bool: for var in variables: if var in n.variables_read: if self.detect_arithmetic_operations(n): - return True, n + return n node.pop() - return False, None - + return None + def generate_informative_messages(self, spot_price_classes): messages = [] for spot_price in spot_price_classes: @@ -176,7 +184,7 @@ def generate_informative_messages(self, spot_price_classes): f"Spot price usage detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}. It seems like trying to obtain data through balanceOf method.\n" ) return messages - + def generate_calc_messages(self, node): return f"Calculations are made with spot price data in {node.source_mapping}\n" @@ -187,7 +195,7 @@ def _detect(self): messages = self.generate_informative_messages(spot_price_usage) for spot_price in spot_price_usage: - true, node = self.are_calculations_made_with_spot_data(spot_price.node) + node = self.are_calculations_made_with_spot_data(spot_price.node) if node is not None: self.IMPACT = DetectorClassification.HIGH messages.append(self.generate_calc_messages(node)) diff --git a/tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol similarity index 100% rename from tests/e2e/detectors/test_data/spot_price/0.6.12/spot_price_getReserves.sol rename to tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_slot0.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_slot0.sol new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol new file mode 100644 index 0000000000..7478a15359 --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol @@ -0,0 +1,43 @@ +pragma solidity 0.8.20; + +interface IERC20 { + event Transfer(address indexed from, address indexed to, uint256 value); + + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 value) external returns (bool); + + function allowance( + address owner, + address spender + ) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); +} + +contract BalanceOfData { + function getPriceV2( + address pool, + address tokenAddress, + address weth + ) internal view returns (uint256 price) { + uint256 moteBalance = IERC20(tokenAddress).balanceOf(pool); + uint256 ethBalance = IERC20(weth).balanceOf(pool); + + price = (ethBalance * 10 ** 18) / moteBalance; + } +} From 947e4476b39d2dd8ea65332fa7b852813fed220b Mon Sep 17 00:00:00 2001 From: Talfao Date: Mon, 8 Apr 2024 20:48:19 +0200 Subject: [PATCH 44/59] feat: making analysis faster --- slither/detectors/oracles/spot_price.py | 159 +++++++++++------- ...r_0_6_12_spot_price_getReserves_sol__0.txt | 7 + ...tor_0_8_20_spot_price_balanceOf_sol__0.txt | 4 + .../0.6.12/spot_price_getReserves.sol | 2 +- .../spot_price_getReserves.sol-0.6.12.zip | Bin 0 -> 13027 bytes .../0.8.20/spot_price_balanceOf.sol | 6 +- .../spot_price_balanceOf.sol-0.8.20.zip | Bin 0 -> 3706 bytes tests/e2e/detectors/test_detectors.py | 2 + 8 files changed, 112 insertions(+), 68 deletions(-) create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol-0.6.12.zip create mode 100644 tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol-0.8.20.zip diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 1f9df4a3fa..4013fca602 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -1,10 +1,7 @@ from typing import List -from slither.analyses.data_dependency.data_dependency import get_dependencies -from slither.core.declarations.contract import Contract from slither.core.declarations.function_contract import FunctionContract -from slither.core.variables.state_variable import StateVariable from slither.detectors.abstract_detector import AbstractDetector -from slither.slithir.operations import HighLevelCall, InternalCall, Operation, Unpack +from slither.slithir.operations import HighLevelCall, Operation, LibraryCall from slither.slithir.variables.variable import Variable from slither.core.cfg.node import Node from slither.detectors.abstract_detector import DetectorClassification @@ -15,11 +12,18 @@ # SpotPriceUsage class to store the node and interface +# For better readability of messages class SpotPriceUsage: def __init__(self, node: Node, interface: str): self.node = node self.interface = interface + def mapping(self): + return self.node.source_mapping + + def type_of_interface(self): + return self.interface + class SpotPriceDetector(AbstractDetector): """ @@ -39,18 +43,17 @@ class SpotPriceDetector(AbstractDetector): WIKI_DESCRIPTION = "Detection of spot price usage" WIKI_RECOMMENDATION = "Using spot price for calculations can lead to vulnerabilities. Make sure to validate the data before using it or consider use of TWAP oracles." - - def detect_uniswap(self, function: FunctionContract, function_call, interface) -> Node: - interface = "IUniswapV3Pool" - function_call = "slot0" - return self.wanted_intersting_node_call(function, function_call, interface) - - def detect_uniswap_v2(self, function: FunctionContract) -> Node: - interface = "IUniswapV2Pair" - function_call = "getReserves" - return self.wanted_intersting_node_call(function, function_call, interface) - - def instance_of_call(self, ir: Operation, function_name, interface_name) -> bool: + # SafeMath functions for compatibility with solidity contracts < 0.8.0 version + SAFEMATH_FUNCTIONS = ["add", "sub", "mul", "div"] + # Uniswap interfaces + UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] + # Suspicious calls for Uniswap + UNISWAP_SUSPICIOUS_CALLS = ["slot0", "getReserves"] + + # Check if the instance of the call is made, and if the function name and interface name are the same + # Or if one of them at least matches + @staticmethod + def instance_of_call(ir: Operation, function_name, interface_name) -> bool: if isinstance(ir, HighLevelCall): if isinstance(ir.destination, Variable): if function_name is not None and interface_name is not None: @@ -68,44 +71,65 @@ def instance_of_call(self, ir: Operation, function_name, interface_name) -> bool return False - def get_argument_of_high_level_call(self, ir: Operation) -> List[Variable]: + # Get the arguments of the high level call + @staticmethod + def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: if isinstance(ir, HighLevelCall): return ir.arguments return [] - def balance_of_spot_price(self, function) -> bool: + # Detect balanceOf method usage + # def balance_of_spot_price(self, function) -> bool: + + # for node in function.nodes: + # for ir in node.irs: + # # The indication of balanceOf method usage as spot price is by calling + # # this functions two times in row for same argument + + # return first_node, second_node + + # Detect oracle call + def detect_oracle_call(self, function: FunctionContract, function_names, interface_names) -> (Node, str): + nodes = [] first_node = None second_node = None first_arguments = [] for node in function.nodes: for ir in node.irs: - if self.instance_of_call(ir, "balanceOf", None): - arguments = self.get_argument_of_high_level_call(ir) - if first_node is not None and arguments[0] == first_arguments[0]: - second_node = node - return first_node, second_node - first_arguments = arguments - first_node = node - return first_node, second_node - - def wanted_intersting_node_call( - self, function: FunctionContract, function_name, interface_name - ) -> Node: - for node in function.nodes: - for ir in node.irs: - if self.instance_of_call(ir, function_name, interface_name): - return node - return None - - def detect_any_fork_of_uniswap(self, function: FunctionContract, found_nodes) -> (Node, str): - node = self.wanted_intersting_node_call(function, "getReserves", None) - if node is None: - node = self.wanted_intersting_node_call(function, "slot0", None) - - for n in found_nodes: - if n.node == node: - return None, None - return node, "Fork" + for i in range(len(function_names)): + function_name = function_names[i] + interface_name = interface_names[i] + + # Detect UniswapV3 or UniswapV2 + if self.instance_of_call(ir, function_name, interface_name): + nodes.append((node, interface_name)) + + # Detect any fork of Uniswap + elif self.instance_of_call(ir, function_name, None): + nodes.append((node, None)) + + elif self.instance_of_call(ir, "balanceOf", None): + arguments = self.get_argument_of_high_level_call(ir) + if first_node is not None and arguments[0] == first_arguments[0]: + second_node = node + nodes.append(([first_node, second_node], "BalanceOF")) + first_node = None + second_node = None + first_arguments = [] + else: + first_arguments = arguments + first_node = node + return nodes + + # def detect_any_fork_of_uniswap(self, function: FunctionContract, found_nodes) -> (Node, str): + # node = self.detect_oracle_call(function, "getReserves", None) + # if node is None: + # node = self.detect_oracle_call(function, "slot0", None) + + # for n in found_nodes: + # if n.node == node: + # return None, None + # return node, "Fork" # Detect spot price usage # 1. Detect Uniswap V3 @@ -116,26 +140,19 @@ def detect_spot_price_usage(self): spot_price_usage = [] for contract in self.contracts: for function in contract.functions: - node_uniswap_v3 = SpotPriceUsage(self.detect_uniswap_v3(function), "IUniswapV3Pool") - if node_uniswap_v3.node is not None: - spot_price_usage.append(node_uniswap_v3) - - node_uniswap_v2 = SpotPriceUsage(self.detect_uniswap_v2(function), "IUniswapV2Pair") - if node_uniswap_v2.node is not None: - spot_price_usage.append(node_uniswap_v2) - - node_fork = self.detect_any_fork_of_uniswap(function, spot_price_usage) - node_fork = SpotPriceUsage(node_fork[0], node_fork[1]) - if node_fork.node is not None: - spot_price_usage.append(node_fork) - - node1, node2 = self.balance_of_spot_price(function) - if node1 is not None and node2 is not None: - spot_price_usage.append(SpotPriceUsage([node1, node2], "BalanceOF")) + oracle_call = self.detect_oracle_call( + function, + ["slot0", "getReserves"], + ["IUniswapV3Pool", "IUniswapV2Pair"], + ) + for call in oracle_call: + spot_price_usage.append(SpotPriceUsage(call[0], call[1])) return spot_price_usage + # Check if arithmetic operations are made + # Compatibility with SafeMath library def detect_arithmetic_operations(self, node: Node) -> bool: for ir in node.irs: if isinstance(ir, Binary): @@ -146,12 +163,21 @@ def detect_arithmetic_operations(self, node: Node) -> bool: BinaryType.DIVISION, ): return True + elif isinstance(ir, LibraryCall): + if hasattr(ir, "function"): + if ir.function.name in self.SAFEMATH_FUNCTIONS: + return True return False + # Check if calculations are made with spot data def are_calculations_made_with_spot_data(self, node: Node) -> Node: + + # For the case when the node is not a list, create a list + # This is done to make compatibility with balanceOf method usage which returns two nodes if not isinstance(node, list): node = [node] + # Check if the node is used in calculations while node: variables = node[0].variables_written for n in node[0].function.nodes: @@ -164,7 +190,9 @@ def are_calculations_made_with_spot_data(self, node: Node) -> Node: node.pop() return None - def generate_informative_messages(self, spot_price_classes): + # Generate informative messages for the detected spot price usage + @staticmethod + def generate_informative_messages(spot_price_classes): messages = [] for spot_price in spot_price_classes: if spot_price.interface == "IUniswapV3Pool": @@ -181,11 +209,13 @@ def generate_informative_messages(self, spot_price_classes): ) elif spot_price.interface == "BalanceOF": messages.append( - f"Spot price usage detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}. It seems like trying to obtain data through balanceOf method.\n" + f"Spot price usage detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n" ) return messages - def generate_calc_messages(self, node): + # Generate message for the node which occured in calculations + @staticmethod + def generate_calc_messages(node): return f"Calculations are made with spot price data in {node.source_mapping}\n" def _detect(self): @@ -199,6 +229,7 @@ def _detect(self): if node is not None: self.IMPACT = DetectorClassification.HIGH messages.append(self.generate_calc_messages(node)) + messages = list(set(messages)) res = self.generate_result(messages) results.append(res) return results diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt new file mode 100644 index 0000000000..b9da215849 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt @@ -0,0 +1,7 @@ +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#204-206 +Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#196 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#196. +Spot price usage detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#197 +Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#201-203 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#201-203. +Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#204-206 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#204-206. +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#200 + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt new file mode 100644 index 0000000000..7325373ae0 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt @@ -0,0 +1,4 @@ +Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#38 and tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#38. +Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#39 and tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#39. +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#41 + diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol index fce6d4410b..8680e52582 100644 --- a/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol +++ b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol @@ -197,7 +197,7 @@ contract YaxisVotePower is IVoteProxy { (uint256 _yaxReserves, , ) = yaxisEthUniswapV2Pair.getReserves(); uint256 _supply = yaxisEthUniswapV2Pair.totalSupply(); _supply = _supply == 0 ? 1e18 : _supply; - uint256 _lpStakingYax = (_yaxReserves * _stakeAmount) / _supply; + uint256 _lpStakingYax = _yaxReserves.mul(_stakeAmount).div(_supply); uint256 _rewardsYaxisAmount = rewardsYaxis.balanceOf(_voter).add( rewardsYaxis.earned(_voter) ); diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol-0.6.12.zip b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol-0.6.12.zip new file mode 100644 index 0000000000000000000000000000000000000000..f9c65c83bbf18e71c4b8156e8a4f78067324ee7d GIT binary patch literal 13027 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 28dcc5e755..5082f2be98 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1679,6 +1679,8 @@ def id_test(test_item: Test): "return_bomb.sol", "0.8.20", ), + Test(all_detectors.SpotPriceDetector, "spot_price_getReserves.sol", "0.6.12"), + Test(all_detectors.SpotPriceDetector, "spot_price_balanceOf.sol", "0.8.20"), ] GENERIC_PATH = "/GENERIC_PATH" From 2e5e4bcf22cb3e578719ca0c0b1b2c5854a4777a Mon Sep 17 00:00:00 2001 From: Talfao Date: Mon, 15 Apr 2024 10:52:11 +0200 Subject: [PATCH 45/59] fix: remove files which should be in different branch --- slither/detectors/all_detectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 4fdf858ed9..b6e9965d50 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,4 +97,4 @@ from .operations.incorrect_exp import IncorrectOperatorExponentiation from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb -from .oracles.spot_price import SpotPriceDetector \ No newline at end of file +from .oracles.spot_price import SpotPriceDetector From f6401655b8fcfb1b167421f985437c5fa01d9a73 Mon Sep 17 00:00:00 2001 From: Talfao Date: Mon, 15 Apr 2024 12:22:52 +0200 Subject: [PATCH 46/59] fix: balanceOf check --- slither/detectors/oracles/spot_price.py | 41 +++++++++++++------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 4013fca602..47f23c8392 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -44,7 +44,7 @@ class SpotPriceDetector(AbstractDetector): WIKI_RECOMMENDATION = "Using spot price for calculations can lead to vulnerabilities. Make sure to validate the data before using it or consider use of TWAP oracles." # SafeMath functions for compatibility with solidity contracts < 0.8.0 version - SAFEMATH_FUNCTIONS = ["add", "sub", "mul", "div"] + SAFEMATH_FUNCTIONS = ["mul", "div"] # Uniswap interfaces UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] # Suspicious calls for Uniswap @@ -108,17 +108,20 @@ def detect_oracle_call(self, function: FunctionContract, function_names, interfa elif self.instance_of_call(ir, function_name, None): nodes.append((node, None)) - elif self.instance_of_call(ir, "balanceOf", None): - arguments = self.get_argument_of_high_level_call(ir) - if first_node is not None and arguments[0] == first_arguments[0]: - second_node = node - nodes.append(([first_node, second_node], "BalanceOF")) - first_node = None - second_node = None - first_arguments = [] - else: - first_arguments = arguments - first_node = node + if self.instance_of_call(ir, "balanceOf", None): + arguments = self.get_argument_of_high_level_call(ir) + if first_node is not None and arguments[0] == first_arguments[0]: + second_node = node + nodes.append(([first_node, second_node], "BalanceOF")) + first_node = None + second_node = None + first_arguments = [] + else: + first_arguments = arguments + first_node = node + else: + first_node = None + first_arguments = [] return nodes # def detect_any_fork_of_uniswap(self, function: FunctionContract, found_nodes) -> (Node, str): @@ -157,8 +160,6 @@ def detect_arithmetic_operations(self, node: Node) -> bool: for ir in node.irs: if isinstance(ir, Binary): if ir.type in ( - BinaryType.ADDITION, - BinaryType.SUBTRACTION, BinaryType.MULTIPLICATION, BinaryType.DIVISION, ): @@ -197,19 +198,19 @@ def generate_informative_messages(spot_price_classes): for spot_price in spot_price_classes: if spot_price.interface == "IUniswapV3Pool": messages.append( - f"Spot price usage detected in Uniswap V3 at {spot_price.node.source_mapping}\n" + f"Method which could indicate usage of spot price was detected in Uniswap V3 at {spot_price.node.source_mapping}\n{spot_price.node}\n" ) elif spot_price.interface == "IUniswapV2Pair": messages.append( - f"Spot price usage detected in Uniswap V2 at {spot_price.node.source_mapping}\n" + f"Method which could indicate usage of spot price was detected in Uniswap V2 at {spot_price.node.source_mapping}\n{spot_price.node}\n" ) elif spot_price.interface == "Fork": messages.append( - f"Spot price usage detected in Uniswap Fork at {spot_price.node.source_mapping}\n" + f"Method which could indicate usage of spot price was detected in Uniswap Fork at {spot_price.node.source_mapping}\n{spot_price.node}\n" ) elif spot_price.interface == "BalanceOF": messages.append( - f"Spot price usage detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n" + f"Method which could indicate usage of spot price was detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n{spot_price.node[0]}\n{spot_price.node[1]}\n" ) return messages @@ -229,7 +230,9 @@ def _detect(self): if node is not None: self.IMPACT = DetectorClassification.HIGH messages.append(self.generate_calc_messages(node)) - messages = list(set(messages)) + # It can contain duplication, sorted and unique messages. + # Sorting due to testing purposes + messages = sorted(list(set(messages))) res = self.generate_result(messages) results.append(res) return results From 1fa07aad6fb8db8a7f0c3494f256e1c39b8b3571 Mon Sep 17 00:00:00 2001 From: Talfao Date: Mon, 15 Apr 2024 14:41:40 +0200 Subject: [PATCH 47/59] fix: balanceOf issue --- slither/detectors/oracles/spot_price.py | 31 ++++--------------- ...r_0_6_12_spot_price_getReserves_sol__0.txt | 10 +++--- ...tor_0_8_20_spot_price_balanceOf_sol__0.txt | 5 +-- .../0.6.12/spot_price_slot0.sol | 0 4 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_slot0.sol diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 47f23c8392..b8a297bb5c 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -78,16 +78,6 @@ def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: return ir.arguments return [] - # Detect balanceOf method usage - # def balance_of_spot_price(self, function) -> bool: - - # for node in function.nodes: - # for ir in node.irs: - # # The indication of balanceOf method usage as spot price is by calling - # # this functions two times in row for same argument - - # return first_node, second_node - # Detect oracle call def detect_oracle_call(self, function: FunctionContract, function_names, interface_names) -> (Node, str): nodes = [] @@ -116,24 +106,14 @@ def detect_oracle_call(self, function: FunctionContract, function_names, interfa first_node = None second_node = None first_arguments = [] + else: first_arguments = arguments first_node = node - else: - first_node = None - first_arguments = [] + break + return nodes - # def detect_any_fork_of_uniswap(self, function: FunctionContract, found_nodes) -> (Node, str): - # node = self.detect_oracle_call(function, "getReserves", None) - # if node is None: - # node = self.detect_oracle_call(function, "slot0", None) - - # for n in found_nodes: - # if n.node == node: - # return None, None - # return node, "Fork" - # Detect spot price usage # 1. Detect Uniswap V3 # 2. Detect Uniswap V2 @@ -204,7 +184,7 @@ def generate_informative_messages(spot_price_classes): messages.append( f"Method which could indicate usage of spot price was detected in Uniswap V2 at {spot_price.node.source_mapping}\n{spot_price.node}\n" ) - elif spot_price.interface == "Fork": + elif spot_price.interface == None: messages.append( f"Method which could indicate usage of spot price was detected in Uniswap Fork at {spot_price.node.source_mapping}\n{spot_price.node}\n" ) @@ -228,7 +208,8 @@ def _detect(self): for spot_price in spot_price_usage: node = self.are_calculations_made_with_spot_data(spot_price.node) if node is not None: - self.IMPACT = DetectorClassification.HIGH + self.IMPACT = DetectorClassification.LOW + self.CONFIDENCE = DetectorClassification.LOW messages.append(self.generate_calc_messages(node)) # It can contain duplication, sorted and unique messages. # Sorting due to testing purposes diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt index b9da215849..e9f4ae0b86 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt @@ -1,7 +1,7 @@ -Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#204-206 -Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#196 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#196. -Spot price usage detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#197 -Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#201-203 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#201-203. -Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#204-206 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#204-206. Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#200 +Method which could indicate usage of spot price was detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#196 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#201-203. +NEW VARIABLE _stakeAmount = rewardsYaxisEth.balanceOf(_voter) +NEW VARIABLE _rewardsYaxisAmount = rewardsYaxis.balanceOf(_voter).add(rewardsYaxis.earned(_voter)) +Method which could indicate usage of spot price was detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#197 +EXPRESSION (_yaxReserves) = yaxisEthUniswapV2Pair.getReserves() diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt index 7325373ae0..1ad5658d90 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt @@ -1,4 +1,5 @@ -Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#38 and tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#38. -Spot price usage detected at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#39 and tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#39. Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#41 +Method which could indicate usage of spot price was detected at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#38 and tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#39. +NEW VARIABLE usdcBalance = IERC20(USDCAddress).balanceOf(pool) +NEW VARIABLE ethBalance = IERC20(weth).balanceOf(pool) diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_slot0.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_slot0.sol deleted file mode 100644 index e69de29bb2..0000000000 From 51a58bf9981968faee42833767702e233ebe0c8e Mon Sep 17 00:00:00 2001 From: Talfao Date: Mon, 15 Apr 2024 14:43:35 +0200 Subject: [PATCH 48/59] fix: pylint issues --- slither/detectors/oracles/spot_price.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index b8a297bb5c..257a1cd9bd 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -20,7 +20,7 @@ def __init__(self, node: Node, interface: str): def mapping(self): return self.node.source_mapping - + def type_of_interface(self): return self.interface @@ -79,21 +79,23 @@ def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: return [] # Detect oracle call - def detect_oracle_call(self, function: FunctionContract, function_names, interface_names) -> (Node, str): + def detect_oracle_call( + self, function: FunctionContract, function_names, interface_names + ) -> (Node, str): nodes = [] first_node = None second_node = None first_arguments = [] for node in function.nodes: for ir in node.irs: - for i in range(len(function_names)): + for i in range(len(function_names)): # pylint: disable=consider-using-enumerate function_name = function_names[i] interface_name = interface_names[i] # Detect UniswapV3 or UniswapV2 if self.instance_of_call(ir, function_name, interface_name): nodes.append((node, interface_name)) - + # Detect any fork of Uniswap elif self.instance_of_call(ir, function_name, None): nodes.append((node, None)) @@ -106,12 +108,12 @@ def detect_oracle_call(self, function: FunctionContract, function_names, interfa first_node = None second_node = None first_arguments = [] - + else: first_arguments = arguments first_node = node break - + return nodes # Detect spot price usage @@ -129,7 +131,7 @@ def detect_spot_price_usage(self): ["slot0", "getReserves"], ["IUniswapV3Pool", "IUniswapV2Pair"], ) - for call in oracle_call: + for call in oracle_call: spot_price_usage.append(SpotPriceUsage(call[0], call[1])) return spot_price_usage @@ -184,7 +186,7 @@ def generate_informative_messages(spot_price_classes): messages.append( f"Method which could indicate usage of spot price was detected in Uniswap V2 at {spot_price.node.source_mapping}\n{spot_price.node}\n" ) - elif spot_price.interface == None: + elif spot_price.interface is None: messages.append( f"Method which could indicate usage of spot price was detected in Uniswap Fork at {spot_price.node.source_mapping}\n{spot_price.node}\n" ) From 89a6d75f0ed5e524b48c7e38fbfdc7e5fce45610 Mon Sep 17 00:00:00 2001 From: Talfao Date: Tue, 16 Apr 2024 13:34:39 +0200 Subject: [PATCH 49/59] feat: improvement for detecting correct balanceOF scenario --- slither/detectors/oracles/spot_price.py | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 257a1cd9bd..8b54cbc657 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -78,17 +78,23 @@ def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: return ir.arguments return [] + @staticmethod + def different_address(first_ir: Operation, second_ir: Operation): + print(first_ir.destination, second_ir.destination) + if first_ir.destination != second_ir.destination: + return True + return False + # Detect oracle call def detect_oracle_call( self, function: FunctionContract, function_names, interface_names ) -> (Node, str): nodes = [] first_node = None - second_node = None first_arguments = [] for node in function.nodes: for ir in node.irs: - for i in range(len(function_names)): # pylint: disable=consider-using-enumerate + for i in range(len(function_names)): # pylint: disable=consider-using-enumerate function_name = function_names[i] interface_name = interface_names[i] @@ -102,16 +108,21 @@ def detect_oracle_call( if self.instance_of_call(ir, "balanceOf", None): arguments = self.get_argument_of_high_level_call(ir) - if first_node is not None and arguments[0] == first_arguments[0]: - second_node = node - nodes.append(([first_node, second_node], "BalanceOF")) + if ( + first_node is not None + and arguments[0] == first_arguments[0] + and self.different_address(first_node[1], ir) + ): + nodes.append(([first_node[0], node], "BalanceOF")) first_node = None - second_node = None first_arguments = [] else: first_arguments = arguments - first_node = node + first_node = ( + node, + ir, + ) # Node and ir which stores destination can be used for address var comparison break return nodes @@ -126,12 +137,12 @@ def detect_spot_price_usage(self): for contract in self.contracts: for function in contract.functions: - oracle_call = self.detect_oracle_call( + oracle_calls = self.detect_oracle_call( function, ["slot0", "getReserves"], ["IUniswapV3Pool", "IUniswapV2Pair"], ) - for call in oracle_call: + for call in oracle_calls: spot_price_usage.append(SpotPriceUsage(call[0], call[1])) return spot_price_usage @@ -192,7 +203,7 @@ def generate_informative_messages(spot_price_classes): ) elif spot_price.interface == "BalanceOF": messages.append( - f"Method which could indicate usage of spot price was detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n{spot_price.node[0]}\n{spot_price.node[1]}\n" + f"Method which could indicate usage of spot price was detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n{spot_price.node[0].irs}\n{spot_price.node[1]}\n" ) return messages From a21fc916aa99ac01476a0abe4201fd1dbaab78f4 Mon Sep 17 00:00:00 2001 From: Talfao Date: Tue, 16 Apr 2024 15:24:38 +0200 Subject: [PATCH 50/59] feat: getAmountOut add as problem + tainting vars --- slither/detectors/oracles/spot_price.py | 42 +++++++++-- .../0.8.20/spot_price_getAmountOut.sol | 73 +++++++++++++++++++ 2 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 8b54cbc657..3c51d22b55 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -1,9 +1,10 @@ from typing import List from slither.core.declarations.function_contract import FunctionContract from slither.detectors.abstract_detector import AbstractDetector -from slither.slithir.operations import HighLevelCall, Operation, LibraryCall +from slither.slithir.operations import HighLevelCall, Operation, LibraryCall, Assignment +from slither.analyses.data_dependency.data_dependency import is_dependent from slither.slithir.variables.variable import Variable -from slither.core.cfg.node import Node +from slither.core.cfg.node import Node, recheable from slither.detectors.abstract_detector import DetectorClassification from slither.slithir.operations import ( Binary, @@ -45,6 +46,9 @@ class SpotPriceDetector(AbstractDetector): # SafeMath functions for compatibility with solidity contracts < 0.8.0 version SAFEMATH_FUNCTIONS = ["mul", "div"] + + # Uniswap calculations functions + CALC_FUNCTIONS = ["getAmountOut"] # Uniswap interfaces UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] # Suspicious calls for Uniswap @@ -163,6 +167,26 @@ def detect_arithmetic_operations(self, node: Node) -> bool: return True return False + def calc_functions(self, node: Node) -> bool: + for ir in node.irs: + if isinstance(ir, HighLevelCall): + if ir.function.name in self.CALC_FUNCTIONS: + return True + return False + + def track_var(self, variable, node) -> bool: + temp_variable = None + for ir in node.irs: + if isinstance(ir, Assignment): + if str(ir.rvalue) == str(variable): + temp_variable = ir.lvalue + if temp_variable is not None: + for v in node.variables_written: + if str(v) == str(temp_variable): + variable = v + return variable + + # Check if calculations are made with spot data def are_calculations_made_with_spot_data(self, node: Node) -> Node: @@ -174,13 +198,19 @@ def are_calculations_made_with_spot_data(self, node: Node) -> Node: # Check if the node is used in calculations while node: variables = node[0].variables_written - for n in node[0].function.nodes: - if n == node[0]: - continue + recheable_nodes = recheable(node[0]) + changed_vars = [] + for n in recheable_nodes: for var in variables: + changed_vars.append(self.track_var(var, n)) + for n in recheable_nodes: + for var in changed_vars: if var in n.variables_read: if self.detect_arithmetic_operations(n): return n + elif self.calc_functions(n): + return n + node.pop() return None @@ -203,7 +233,7 @@ def generate_informative_messages(spot_price_classes): ) elif spot_price.interface == "BalanceOF": messages.append( - f"Method which could indicate usage of spot price was detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n{spot_price.node[0].irs}\n{spot_price.node[1]}\n" + f"Method which could indicate usage of spot price was detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n{spot_price.node[0]}\n{spot_price.node[1]}\n" ) return messages diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol new file mode 100644 index 0000000000..3571ee0be6 --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol @@ -0,0 +1,73 @@ +pragma solidity 0.8.20; + +interface IERC20 { + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + function decimals() external view virtual returns (uint8); +} +interface IUniswapV2Pair { + function getReserves() + external + view + returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); +} +interface IUniswapV2Factory { + function getPair( + address tokenA, + address tokenB + ) external view returns (address pair); +} + +interface IUniswapV2Router01 { + function quote( + uint amountA, + uint reserveA, + uint reserveB + ) external pure returns (uint amountB); + function getAmountOut( + uint amountIn, + uint reserveIn, + uint reserveOut + ) external pure returns (uint amountOut); +} + +contract getAmountOut { + // Same address just for testing purposes + address UNISWAP_ROUTER = + address(0x96871914D0F4354A79B1E4651b464351e093b737); + address UNISWAP_FACTORY = + address(0x96871914D0F4354A79B1E4651b464351e093b737); + address USDC = address(0x96871914D0F4354A79B1E4651b464351e093b737); + address WETH = address(0x96871914D0F4354A79B1E4651b464351e093b737); + + function getEthUsdPrice() public view returns (uint256) { + address pairAddress = IUniswapV2Factory(UNISWAP_FACTORY).getPair( + USDC, + WETH + ); + require(pairAddress != address(0x00), "pair not found"); + IUniswapV2Pair pair = IUniswapV2Pair(pairAddress); + (uint256 left, uint256 right, ) = pair.getReserves(); + (uint256 usdcReserves, uint256 ethReserves) = (USDC < WETH) + ? (left, right) + : (right, left); + uint8 ethDecimals = IERC20(WETH).decimals(); + //uint8 usdcDecimals = ERC20(USDC).decimals(); + //returns price in 6 decimals + return + IUniswapV2Router01(UNISWAP_ROUTER).getAmountOut( + 10 ** ethDecimals, + ethReserves, + usdcReserves + ); + } +} From 784d0e49661bcb11561c06685442a7f8a58d3f1f Mon Sep 17 00:00:00 2001 From: Talfao Date: Tue, 16 Apr 2024 15:53:15 +0200 Subject: [PATCH 51/59] feat: break loop earlier in tracking --- slither/detectors/oracles/spot_price.py | 6 +++++- ...tor_0_8_20_spot_price_getAmountOut_sol__0.txt | 4 ++++ .../spot_price_getAmountOut.sol-0.8.20.zip | Bin 0 -> 8540 bytes tests/e2e/detectors/test_detectors.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol-0.8.20.zip diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 3c51d22b55..df8a79d43e 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -174,12 +174,16 @@ def calc_functions(self, node: Node) -> bool: return True return False - def track_var(self, variable, node) -> bool: + # Track if the variable was assigned to different variable without change + @staticmethod + def track_var(variable, node) -> bool: temp_variable = None for ir in node.irs: if isinstance(ir, Assignment): if str(ir.rvalue) == str(variable): temp_variable = ir.lvalue + else: + return variable if temp_variable is not None: for v in node.variables_written: if str(v) == str(temp_variable): diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt new file mode 100644 index 0000000000..e4565ec39a --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt @@ -0,0 +1,4 @@ +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol#66-71 +Method which could indicate usage of spot price was detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol#59 +EXPRESSION (left,right) = pair.getReserves() + diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..f14e0c61d0318f5467f7dcafef7aefeda2e972e9 GIT binary patch literal 8540 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 5082f2be98..96bf41d58a 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1681,6 +1681,7 @@ def id_test(test_item: Test): ), Test(all_detectors.SpotPriceDetector, "spot_price_getReserves.sol", "0.6.12"), Test(all_detectors.SpotPriceDetector, "spot_price_balanceOf.sol", "0.8.20"), + Test(all_detectors.SpotPriceDetector, "spot_price_getAmountOut.sol", "0.8.20"), ] GENERIC_PATH = "/GENERIC_PATH" From ead9998bb8804c86b64a9dbeb059b1c499e71b52 Mon Sep 17 00:00:00 2001 From: Talfao Date: Wed, 17 Apr 2024 10:45:20 +0200 Subject: [PATCH 52/59] feat: remove cumulativePrice and case when slot0 does not return price --- slither/detectors/oracles/spot_price.py | 37 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index df8a79d43e..f7d6913c22 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -46,9 +46,10 @@ class SpotPriceDetector(AbstractDetector): # SafeMath functions for compatibility with solidity contracts < 0.8.0 version SAFEMATH_FUNCTIONS = ["mul", "div"] - # Uniswap calculations functions CALC_FUNCTIONS = ["getAmountOut"] + # Protected functions + PROTECTED_FUNCTIONS = ["currentCumulativePrices"] # Uniswap interfaces UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] # Suspicious calls for Uniswap @@ -59,6 +60,8 @@ class SpotPriceDetector(AbstractDetector): @staticmethod def instance_of_call(ir: Operation, function_name, interface_name) -> bool: if isinstance(ir, HighLevelCall): + if not hasattr(ir.function, "name") or not hasattr(ir.destination, "type"): + return False if isinstance(ir.destination, Variable): if function_name is not None and interface_name is not None: if ( @@ -84,7 +87,6 @@ def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: @staticmethod def different_address(first_ir: Operation, second_ir: Operation): - print(first_ir.destination, second_ir.destination) if first_ir.destination != second_ir.destination: return True return False @@ -93,6 +95,9 @@ def different_address(first_ir: Operation, second_ir: Operation): def detect_oracle_call( self, function: FunctionContract, function_names, interface_names ) -> (Node, str): + # Ignore cases with TWAP functions + if function.name in self.PROTECTED_FUNCTIONS: + return [] nodes = [] first_node = None first_arguments = [] @@ -174,6 +179,13 @@ def calc_functions(self, node: Node) -> bool: return True return False + # Check if slot0 returned price value + def slot0_returned_price(self, variables) -> bool: + for var in variables: + # sqrtPricex96 is type uint160 and only var of this type is returned by slot0 + if hasattr(var, "type") and str(var.type) == "uint160": + return True + return False # Track if the variable was assigned to different variable without change @staticmethod def track_var(variable, node) -> bool: @@ -192,7 +204,7 @@ def track_var(variable, node) -> bool: # Check if calculations are made with spot data - def are_calculations_made_with_spot_data(self, node: Node) -> Node: + def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> Node: # For the case when the node is not a list, create a list # This is done to make compatibility with balanceOf method usage which returns two nodes @@ -200,8 +212,12 @@ def are_calculations_made_with_spot_data(self, node: Node) -> Node: node = [node] # Check if the node is used in calculations + nodes = [] while node: variables = node[0].variables_written + if interface == "IUniswapV3Pool": + if not self.slot0_returned_price(variables): + return nodes recheable_nodes = recheable(node[0]) changed_vars = [] for n in recheable_nodes: @@ -210,13 +226,15 @@ def are_calculations_made_with_spot_data(self, node: Node) -> Node: for n in recheable_nodes: for var in changed_vars: if var in n.variables_read: + # Check if the variable is used in arithmetic operations if self.detect_arithmetic_operations(n): - return n + nodes.append(n) + # Check if the variable is used in calculation functions elif self.calc_functions(n): - return n + nodes.append(n) node.pop() - return None + return nodes # Generate informative messages for the detected spot price usage @staticmethod @@ -253,11 +271,12 @@ def _detect(self): messages = self.generate_informative_messages(spot_price_usage) for spot_price in spot_price_usage: - node = self.are_calculations_made_with_spot_data(spot_price.node) - if node is not None: + node = self.are_calculations_made_with_spot_data(spot_price.node, spot_price.interface) + if node: self.IMPACT = DetectorClassification.LOW self.CONFIDENCE = DetectorClassification.LOW - messages.append(self.generate_calc_messages(node)) + for n in node: + messages.append(self.generate_calc_messages(n)) # It can contain duplication, sorted and unique messages. # Sorting due to testing purposes messages = sorted(list(set(messages))) From 921f6cb84d9335daa6ee6ad3763a1a722c8a4e62 Mon Sep 17 00:00:00 2001 From: Talfao Date: Wed, 17 Apr 2024 13:42:36 +0200 Subject: [PATCH 53/59] fix: slot0 must return price --- slither/detectors/oracles/spot_price.py | 44 ++++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index f7d6913c22..5358f420e7 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -49,7 +49,7 @@ class SpotPriceDetector(AbstractDetector): # Uniswap calculations functions CALC_FUNCTIONS = ["getAmountOut"] # Protected functions - PROTECTED_FUNCTIONS = ["currentCumulativePrices"] + PROTECTED_FUNCTIONS = ["currentCumulativePrices", "price1CumulativeLast", "price0CumulativeLast"] # Uniswap interfaces UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] # Suspicious calls for Uniswap @@ -78,6 +78,11 @@ def instance_of_call(ir: Operation, function_name, interface_name) -> bool: return False + def ignore_function(self, ir) -> bool: + for function in self.PROTECTED_FUNCTIONS: + if self.instance_of_call(ir, function, None): + return True + return False # Get the arguments of the high level call @staticmethod def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: @@ -87,7 +92,7 @@ def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: @staticmethod def different_address(first_ir: Operation, second_ir: Operation): - if first_ir.destination != second_ir.destination: + if hasattr(first_ir, "destination") and hasattr(first_ir, "destination") and first_ir.destination != second_ir.destination: return True return False @@ -101,14 +106,21 @@ def detect_oracle_call( nodes = [] first_node = None first_arguments = [] + # For finding next node + counter = 0 for node in function.nodes: for ir in node.irs: for i in range(len(function_names)): # pylint: disable=consider-using-enumerate function_name = function_names[i] interface_name = interface_names[i] + if self.ignore_function(ir): + return [] # Detect UniswapV3 or UniswapV2 - if self.instance_of_call(ir, function_name, interface_name): + elif self.instance_of_call(ir, function_name, interface_name): + if interface_name == "IUniswapV3Pool": + if not self.slot0_returned_price(node.variables_written): + continue nodes.append((node, interface_name)) # Detect any fork of Uniswap @@ -120,7 +132,7 @@ def detect_oracle_call( if ( first_node is not None and arguments[0] == first_arguments[0] - and self.different_address(first_node[1], ir) + and self.different_address(first_node[1], ir) and counter == 1 ): nodes.append(([first_node[0], node], "BalanceOF")) first_node = None @@ -132,7 +144,9 @@ def detect_oracle_call( node, ir, ) # Node and ir which stores destination can be used for address var comparison + counter = 0 break + counter += 1 return nodes @@ -170,6 +184,9 @@ def detect_arithmetic_operations(self, node: Node) -> bool: if hasattr(ir, "function"): if ir.function.name in self.SAFEMATH_FUNCTIONS: return True + # if arithmetic_op: + # if "FixedPoint.fraction" in str(node): + # return False return False def calc_functions(self, node: Node) -> bool: @@ -180,12 +197,24 @@ def calc_functions(self, node: Node) -> bool: return False # Check if slot0 returned price value - def slot0_returned_price(self, variables) -> bool: + @staticmethod + def slot0_returned_price(variables) -> bool: for var in variables: # sqrtPricex96 is type uint160 and only var of this type is returned by slot0 if hasattr(var, "type") and str(var.type) == "uint160": return True return False + + # Check getReserves vars + # reserve0 and reserve1 are of type uint112 or someone could directly cast them to uint256 + @staticmethod + def check_reserve_var(var, interface) -> bool: + if interface == "IUniswapV2Pair": + if hasattr(var, "type") and str(var.type) == "uint112" or str(var.type) == "uint256": + return True + else: + return False + return True # Track if the variable was assigned to different variable without change @staticmethod def track_var(variable, node) -> bool: @@ -215,9 +244,6 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No nodes = [] while node: variables = node[0].variables_written - if interface == "IUniswapV3Pool": - if not self.slot0_returned_price(variables): - return nodes recheable_nodes = recheable(node[0]) changed_vars = [] for n in recheable_nodes: @@ -226,6 +252,8 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No for n in recheable_nodes: for var in changed_vars: if var in n.variables_read: + if not self.check_reserve_var(var, interface): + continue # Check if the variable is used in arithmetic operations if self.detect_arithmetic_operations(n): nodes.append(n) From 9e0d4466310533a8483f6c0453c0741572e61e99 Mon Sep 17 00:00:00 2001 From: Talfao Date: Sun, 21 Apr 2024 16:34:09 +0200 Subject: [PATCH 54/59] feat: get calculate functions --- slither/detectors/oracles/spot_price.py | 71 ++++++++++++++----- ...r_0_6_12_spot_price_getReserves_sol__0.txt | 3 - ...tor_0_8_20_spot_price_balanceOf_sol__0.txt | 2 +- ..._0_8_20_spot_price_getAmountOut_sol__0.txt | 2 +- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 5358f420e7..223641759e 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -4,13 +4,16 @@ from slither.slithir.operations import HighLevelCall, Operation, LibraryCall, Assignment from slither.analyses.data_dependency.data_dependency import is_dependent from slither.slithir.variables.variable import Variable -from slither.core.cfg.node import Node, recheable +from slither.core.cfg.node import Node, recheable, NodeType from slither.detectors.abstract_detector import DetectorClassification from slither.slithir.operations import ( Binary, BinaryType, ) +from slither.core.declarations.function import Function + + # SpotPriceUsage class to store the node and interface # For better readability of messages @@ -47,8 +50,8 @@ class SpotPriceDetector(AbstractDetector): # SafeMath functions for compatibility with solidity contracts < 0.8.0 version SAFEMATH_FUNCTIONS = ["mul", "div"] # Uniswap calculations functions - CALC_FUNCTIONS = ["getAmountOut"] - # Protected functions + CALC_FUNCTIONS = ["getAmountOut", "getAmountsOut"] + # Protected functions -> Indicating TWAP V2 oracles PROTECTED_FUNCTIONS = ["currentCumulativePrices", "price1CumulativeLast", "price0CumulativeLast"] # Uniswap interfaces UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] @@ -96,13 +99,14 @@ def different_address(first_ir: Operation, second_ir: Operation): return True return False + + + # Detect oracle call def detect_oracle_call( self, function: FunctionContract, function_names, interface_names ) -> (Node, str): - # Ignore cases with TWAP functions - if function.name in self.PROTECTED_FUNCTIONS: - return [] + nodes = [] first_node = None first_arguments = [] @@ -147,6 +151,7 @@ def detect_oracle_call( counter = 0 break counter += 1 + return nodes @@ -229,9 +234,25 @@ def track_var(variable, node) -> bool: for v in node.variables_written: if str(v) == str(temp_variable): variable = v + print(variable) return variable - + @staticmethod + # Check if calculations are linked to return, that would indicate only get/calculation function + def are_calcs_linked_to_return(node: Node) -> bool: + function = node.function + variables = node.variables_written + returned_vars = function.returns + for r_var in returned_vars: + for var in variables: + if is_dependent(r_var, var, function): + return function + if node.type == NodeType.RETURN: + return function + for s in node.sons: + if s.type == NodeType.RETURN: + return function + return None # Check if calculations are made with spot data def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> Node: @@ -242,6 +263,7 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No # Check if the node is used in calculations nodes = [] + return_functions = [] while node: variables = node[0].variables_written recheable_nodes = recheable(node[0]) @@ -260,10 +282,19 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No # Check if the variable is used in calculation functions elif self.calc_functions(n): nodes.append(n) - node.pop() - return nodes - + for node in nodes: + function = self.are_calcs_linked_to_return(node) + return_functions.append(function) + return nodes, return_functions + + @staticmethod + def only_return(function: Function) -> bool: + if function is None: + return False + if function.reachable_from_functions: + return False + return True # Generate informative messages for the detected spot price usage @staticmethod def generate_informative_messages(spot_price_classes): @@ -287,9 +318,13 @@ def generate_informative_messages(spot_price_classes): ) return messages - # Generate message for the node which occured in calculations + + # Generate message for the node which occured in calculations @staticmethod - def generate_calc_messages(node): + def generate_calc_messages(node: Node, only_return: bool) -> str: + if only_return: + return f"Calculations are made with spot price data in {node.source_mapping} but the function is not used anywhere in the contract.\n" + return f"Calculations are made with spot price data in {node.source_mapping}\n" def _detect(self): @@ -299,12 +334,12 @@ def _detect(self): messages = self.generate_informative_messages(spot_price_usage) for spot_price in spot_price_usage: - node = self.are_calculations_made_with_spot_data(spot_price.node, spot_price.interface) - if node: - self.IMPACT = DetectorClassification.LOW - self.CONFIDENCE = DetectorClassification.LOW - for n in node: - messages.append(self.generate_calc_messages(n)) + nodes, return_functions = self.are_calculations_made_with_spot_data(spot_price.node, spot_price.interface) + if nodes: + for i in range(len(nodes)): + only_return = self.only_return(return_functions[i]) + messages.append(self.generate_calc_messages(nodes[i], only_return )) + # It can contain duplication, sorted and unique messages. # Sorting due to testing purposes messages = sorted(list(set(messages))) diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt index e9f4ae0b86..fbe5b772e4 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt @@ -1,7 +1,4 @@ Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#200 -Method which could indicate usage of spot price was detected at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#196 and tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#201-203. -NEW VARIABLE _stakeAmount = rewardsYaxisEth.balanceOf(_voter) -NEW VARIABLE _rewardsYaxisAmount = rewardsYaxis.balanceOf(_voter).add(rewardsYaxis.earned(_voter)) Method which could indicate usage of spot price was detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#197 EXPRESSION (_yaxReserves) = yaxisEthUniswapV2Pair.getReserves() diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt index 1ad5658d90..4e62bfe1ae 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt @@ -1,4 +1,4 @@ -Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#41 +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#41 but the function is not used anywhere in the contract. Method which could indicate usage of spot price was detected at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#38 and tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#39. NEW VARIABLE usdcBalance = IERC20(USDCAddress).balanceOf(pool) NEW VARIABLE ethBalance = IERC20(weth).balanceOf(pool) diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt index e4565ec39a..7c9cb5d221 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt @@ -1,4 +1,4 @@ -Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol#66-71 +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol#66-71 but the function is not used anywhere in the contract. Method which could indicate usage of spot price was detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol#59 EXPRESSION (left,right) = pair.getReserves() From f042e6ca8ef55b1bffd09128ae2da0eefc38db71 Mon Sep 17 00:00:00 2001 From: Talfao Date: Sun, 21 Apr 2024 16:45:10 +0200 Subject: [PATCH 55/59] fix: pure and view evaluating --- slither/detectors/oracles/spot_price.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 223641759e..741ede94db 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -292,9 +292,11 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No def only_return(function: Function) -> bool: if function is None: return False - if function.reachable_from_functions: - return False - return True + + if (function.view or function.pure) and not function.reachable_from_functions: + return True + + return False # Generate informative messages for the detected spot price usage @staticmethod def generate_informative_messages(spot_price_classes): From 70c187e4d09b2aa19b8727fb920c7b674397ad30 Mon Sep 17 00:00:00 2001 From: Talfao Date: Sun, 21 Apr 2024 16:54:25 +0200 Subject: [PATCH 56/59] fix: reformating --- slither/detectors/oracles/spot_price.py | 77 +++++++++++++------------ 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 741ede94db..4b96967a66 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -14,7 +14,6 @@ from slither.core.declarations.function import Function - # SpotPriceUsage class to store the node and interface # For better readability of messages class SpotPriceUsage: @@ -52,7 +51,11 @@ class SpotPriceDetector(AbstractDetector): # Uniswap calculations functions CALC_FUNCTIONS = ["getAmountOut", "getAmountsOut"] # Protected functions -> Indicating TWAP V2 oracles - PROTECTED_FUNCTIONS = ["currentCumulativePrices", "price1CumulativeLast", "price0CumulativeLast"] + PROTECTED_FUNCTIONS = [ + "currentCumulativePrices", + "price1CumulativeLast", + "price0CumulativeLast", + ] # Uniswap interfaces UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] # Suspicious calls for Uniswap @@ -86,6 +89,7 @@ def ignore_function(self, ir) -> bool: if self.instance_of_call(ir, function, None): return True return False + # Get the arguments of the high level call @staticmethod def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: @@ -95,18 +99,19 @@ def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: @staticmethod def different_address(first_ir: Operation, second_ir: Operation): - if hasattr(first_ir, "destination") and hasattr(first_ir, "destination") and first_ir.destination != second_ir.destination: + if ( + hasattr(first_ir, "destination") + and hasattr(first_ir, "destination") + and first_ir.destination != second_ir.destination + ): return True return False - - - # Detect oracle call def detect_oracle_call( self, function: FunctionContract, function_names, interface_names ) -> (Node, str): - + nodes = [] first_node = None first_arguments = [] @@ -121,10 +126,11 @@ def detect_oracle_call( if self.ignore_function(ir): return [] # Detect UniswapV3 or UniswapV2 - elif self.instance_of_call(ir, function_name, interface_name): - if interface_name == "IUniswapV3Pool": - if not self.slot0_returned_price(node.variables_written): - continue + if self.instance_of_call(ir, function_name, interface_name): + if interface_name == "IUniswapV3Pool" and not self.slot0_returned_price( + node.variables_written + ): + continue nodes.append((node, interface_name)) # Detect any fork of Uniswap @@ -136,7 +142,8 @@ def detect_oracle_call( if ( first_node is not None and arguments[0] == first_arguments[0] - and self.different_address(first_node[1], ir) and counter == 1 + and self.different_address(first_node[1], ir) + and counter == 1 ): nodes.append(([first_node[0], node], "BalanceOF")) first_node = None @@ -151,7 +158,6 @@ def detect_oracle_call( counter = 0 break counter += 1 - return nodes @@ -200,7 +206,7 @@ def calc_functions(self, node: Node) -> bool: if ir.function.name in self.CALC_FUNCTIONS: return True return False - + # Check if slot0 returned price value @staticmethod def slot0_returned_price(variables) -> bool: @@ -209,17 +215,13 @@ def slot0_returned_price(variables) -> bool: if hasattr(var, "type") and str(var.type) == "uint160": return True return False - + # Check getReserves vars # reserve0 and reserve1 are of type uint112 or someone could directly cast them to uint256 @staticmethod - def check_reserve_var(var, interface) -> bool: - if interface == "IUniswapV2Pair": - if hasattr(var, "type") and str(var.type) == "uint112" or str(var.type) == "uint256": - return True - else: - return False - return True + def check_reserve_var(var) -> bool: + return hasattr(var, "type") and str(var.type) == "uint112" or str(var.type) == "uint256" + # Track if the variable was assigned to different variable without change @staticmethod def track_var(variable, node) -> bool: @@ -228,7 +230,7 @@ def track_var(variable, node) -> bool: if isinstance(ir, Assignment): if str(ir.rvalue) == str(variable): temp_variable = ir.lvalue - else: + else: return variable if temp_variable is not None: for v in node.variables_written: @@ -253,6 +255,7 @@ def are_calcs_linked_to_return(node: Node) -> bool: if s.type == NodeType.RETURN: return function return None + # Check if calculations are made with spot data def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> Node: @@ -274,7 +277,7 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No for n in recheable_nodes: for var in changed_vars: if var in n.variables_read: - if not self.check_reserve_var(var, interface): + if interface == "IUniswapV2Pair" and not self.check_reserve_var(var): continue # Check if the variable is used in arithmetic operations if self.detect_arithmetic_operations(n): @@ -283,20 +286,21 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No elif self.calc_functions(n): nodes.append(n) node.pop() - for node in nodes: - function = self.are_calcs_linked_to_return(node) + for node2 in nodes: + function = self.are_calcs_linked_to_return(node2) return_functions.append(function) return nodes, return_functions - + @staticmethod def only_return(function: Function) -> bool: if function is None: return False - + if (function.view or function.pure) and not function.reachable_from_functions: return True - + return False + # Generate informative messages for the detected spot price usage @staticmethod def generate_informative_messages(spot_price_classes): @@ -320,13 +324,12 @@ def generate_informative_messages(spot_price_classes): ) return messages - - # Generate message for the node which occured in calculations + # Generate message for the node which occured in calculations @staticmethod - def generate_calc_messages(node: Node, only_return: bool) -> str: + def generate_calc_messages(node: Node, only_return: bool) -> str: if only_return: return f"Calculations are made with spot price data in {node.source_mapping} but the function is not used anywhere in the contract.\n" - + return f"Calculations are made with spot price data in {node.source_mapping}\n" def _detect(self): @@ -336,11 +339,13 @@ def _detect(self): messages = self.generate_informative_messages(spot_price_usage) for spot_price in spot_price_usage: - nodes, return_functions = self.are_calculations_made_with_spot_data(spot_price.node, spot_price.interface) + nodes, return_functions = self.are_calculations_made_with_spot_data( + spot_price.node, spot_price.interface + ) if nodes: - for i in range(len(nodes)): + for i in range(len(nodes)): # pylint: disable=consider-using-enumerate only_return = self.only_return(return_functions[i]) - messages.append(self.generate_calc_messages(nodes[i], only_return )) + messages.append(self.generate_calc_messages(nodes[i], only_return)) # It can contain duplication, sorted and unique messages. # Sorting due to testing purposes From 0d1e276b553fe332f43b5675d806e78ba7a9a0e9 Mon Sep 17 00:00:00 2001 From: Talfao Date: Tue, 23 Apr 2024 21:23:11 +0200 Subject: [PATCH 57/59] Swap detection --- slither/detectors/oracles/spot_price.py | 51 ++++++++++++++++++------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index 4b96967a66..ce2ad5fd15 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -106,6 +106,7 @@ def different_address(first_ir: Operation, second_ir: Operation): ): return True return False + # Detect oracle call def detect_oracle_call( @@ -115,6 +116,7 @@ def detect_oracle_call( nodes = [] first_node = None first_arguments = [] + swap_indicators = [] # For finding next node counter = 0 for node in function.nodes: @@ -135,7 +137,15 @@ def detect_oracle_call( # Detect any fork of Uniswap elif self.instance_of_call(ir, function_name, None): - nodes.append((node, None)) + if interface_name == "IUniswapV3Pool" and not self.slot0_returned_price( + node.variables_written + ): + continue + nodes.append((node, interface_name)) + + # Swap indication + elif self.instance_of_call(ir, "swap", None): + swap_indicators = [node] if self.instance_of_call(ir, "balanceOf", None): arguments = self.get_argument_of_high_level_call(ir) @@ -158,8 +168,10 @@ def detect_oracle_call( counter = 0 break counter += 1 + - return nodes + + return nodes, swap_indicators # Detect spot price usage # 1. Detect Uniswap V3 @@ -168,19 +180,23 @@ def detect_oracle_call( # 4. Detect balanceOf method usage which can indicate spot price usage in certain cases def detect_spot_price_usage(self): spot_price_usage = [] + swap_functions = [] for contract in self.contracts: for function in contract.functions: - oracle_calls = self.detect_oracle_call( + oracle_calls, swap_function = self.detect_oracle_call( function, ["slot0", "getReserves"], ["IUniswapV3Pool", "IUniswapV2Pair"], ) + uniswap = False for call in oracle_calls: spot_price_usage.append(SpotPriceUsage(call[0], call[1])) - - return spot_price_usage - + if call[1] in self.UNISWAP_INTERFACES: + uniswap = True + if uniswap and swap_function: + swap_functions.append(function) + return spot_price_usage, swap_functions # Check if arithmetic operations are made # Compatibility with SafeMath library def detect_arithmetic_operations(self, node: Node) -> bool: @@ -221,7 +237,7 @@ def slot0_returned_price(variables) -> bool: @staticmethod def check_reserve_var(var) -> bool: return hasattr(var, "type") and str(var.type) == "uint112" or str(var.type) == "uint256" - + # Track if the variable was assigned to different variable without change @staticmethod def track_var(variable, node) -> bool: @@ -303,25 +319,34 @@ def only_return(function: Function) -> bool: # Generate informative messages for the detected spot price usage @staticmethod - def generate_informative_messages(spot_price_classes): + def generate_informative_messages(spot_price_classes, swap_functions): messages = [] + additional_message = "" for spot_price in spot_price_classes: + if not isinstance(spot_price.node, list): + node = [spot_price.node] + else: + node = spot_price.node + for function in swap_functions: + if node[0].function == function: + additional_message = " inside function where performed swap operation" if spot_price.interface == "IUniswapV3Pool": messages.append( - f"Method which could indicate usage of spot price was detected in Uniswap V3 at {spot_price.node.source_mapping}\n{spot_price.node}\n" + f"Method which could indicate usage of spot price was detected in Uniswap V3 at {spot_price.node.source_mapping}{additional_message}\n{spot_price.node}\n" ) elif spot_price.interface == "IUniswapV2Pair": messages.append( - f"Method which could indicate usage of spot price was detected in Uniswap V2 at {spot_price.node.source_mapping}\n{spot_price.node}\n" + f"Method which could indicate usage of spot price was detected in Uniswap V2 at {spot_price.node.source_mapping}{additional_message}\n{spot_price.node}\n" ) elif spot_price.interface is None: messages.append( - f"Method which could indicate usage of spot price was detected in Uniswap Fork at {spot_price.node.source_mapping}\n{spot_price.node}\n" + f"Method which could indicate usage of spot price was detected in Uniswap Fork at {spot_price.node.source_mapping}{additional_message}\n{spot_price.node}\n" ) elif spot_price.interface == "BalanceOF": messages.append( f"Method which could indicate usage of spot price was detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n{spot_price.node[0]}\n{spot_price.node[1]}\n" ) + additional_message = "" return messages # Generate message for the node which occured in calculations @@ -334,9 +359,9 @@ def generate_calc_messages(node: Node, only_return: bool) -> str: def _detect(self): results = [] - spot_price_usage = self.detect_spot_price_usage() + spot_price_usage, swap_functions = self.detect_spot_price_usage() if spot_price_usage: - messages = self.generate_informative_messages(spot_price_usage) + messages = self.generate_informative_messages(spot_price_usage, swap_functions) for spot_price in spot_price_usage: nodes, return_functions = self.are_calculations_made_with_spot_data( From 070b785b090d58e3c6cbf9261371c9365978cf31 Mon Sep 17 00:00:00 2001 From: Talfao Date: Fri, 26 Apr 2024 21:09:32 +0200 Subject: [PATCH 58/59] feat: add more comments before merge --- slither/detectors/oracles/spot_price.py | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py index ce2ad5fd15..f5318df727 100644 --- a/slither/detectors/oracles/spot_price.py +++ b/slither/detectors/oracles/spot_price.py @@ -30,7 +30,7 @@ def type_of_interface(self): class SpotPriceDetector(AbstractDetector): """ - Documentation + Documentation: This detector is used to detect the usage of spot price in the contract. """ ARGUMENT = ( @@ -84,6 +84,7 @@ def instance_of_call(ir: Operation, function_name, interface_name) -> bool: return False + # Ignore functions related to calculation of TWAP def ignore_function(self, ir) -> bool: for function in self.PROTECTED_FUNCTIONS: if self.instance_of_call(ir, function, None): @@ -97,6 +98,7 @@ def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: return ir.arguments return [] + # Check if the address of the destination is different @staticmethod def different_address(first_ir: Operation, second_ir: Operation): if ( @@ -106,7 +108,6 @@ def different_address(first_ir: Operation, second_ir: Operation): ): return True return False - # Detect oracle call def detect_oracle_call( @@ -125,6 +126,7 @@ def detect_oracle_call( function_name = function_names[i] interface_name = interface_names[i] + # Ignore functions related to calculation of TWAP if self.ignore_function(ir): return [] # Detect UniswapV3 or UniswapV2 @@ -142,13 +144,15 @@ def detect_oracle_call( ): continue nodes.append((node, interface_name)) - + # Swap indication elif self.instance_of_call(ir, "swap", None): swap_indicators = [node] + # Detection of balanceOf spot price pattern if self.instance_of_call(ir, "balanceOf", None): arguments = self.get_argument_of_high_level_call(ir) + # Node need to be set and argument of the call need to be the same as the first one if ( first_node is not None and arguments[0] == first_arguments[0] @@ -160,16 +164,15 @@ def detect_oracle_call( first_arguments = [] else: - first_arguments = arguments + first_arguments = arguments # Store arguments for comparison first_node = ( node, ir, ) # Node and ir which stores destination can be used for address var comparison counter = 0 break + # Counter used to check if the next node is immediately after the first node to follow pattern counter += 1 - - return nodes, swap_indicators @@ -197,6 +200,7 @@ def detect_spot_price_usage(self): if uniswap and swap_function: swap_functions.append(function) return spot_price_usage, swap_functions + # Check if arithmetic operations are made # Compatibility with SafeMath library def detect_arithmetic_operations(self, node: Node) -> bool: @@ -237,7 +241,7 @@ def slot0_returned_price(variables) -> bool: @staticmethod def check_reserve_var(var) -> bool: return hasattr(var, "type") and str(var.type) == "uint112" or str(var.type) == "uint256" - + # Track if the variable was assigned to different variable without change @staticmethod def track_var(variable, node) -> bool: @@ -283,13 +287,17 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No # Check if the node is used in calculations nodes = [] return_functions = [] + + # Loop because of BalanceOF method usage as it returns two nodes while node: variables = node[0].variables_written recheable_nodes = recheable(node[0]) changed_vars = [] + # Track the variable if it was assigned to different variable without change for n in recheable_nodes: for var in variables: changed_vars.append(self.track_var(var, n)) + # Check if the variable is used in arithmetic operations or calculate function for n in recheable_nodes: for var in changed_vars: if var in n.variables_read: @@ -302,11 +310,13 @@ def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> No elif self.calc_functions(n): nodes.append(n) node.pop() + # Check if the spot price data are returned for node2 in nodes: function = self.are_calcs_linked_to_return(node2) return_functions.append(function) return nodes, return_functions + # Check if the function, where the spot price data is obtained is used anywhere @staticmethod def only_return(function: Function) -> bool: if function is None: @@ -322,11 +332,14 @@ def only_return(function: Function) -> bool: def generate_informative_messages(spot_price_classes, swap_functions): messages = [] additional_message = "" + # Iterate through all spot price method occuriences for spot_price in spot_price_classes: if not isinstance(spot_price.node, list): node = [spot_price.node] else: node = spot_price.node + # Check if the function is in the swap functions + # Statement add for informative purposes for function in swap_functions: if node[0].function == function: additional_message = " inside function where performed swap operation" From a8a77223e608559ea86df4566a40bfbf86fa729e Mon Sep 17 00:00:00 2001 From: Talfao Date: Sat, 27 Apr 2024 09:44:05 +0200 Subject: [PATCH 59/59] fix: rebase branch --- slither/detectors/all_detectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index b6e9965d50..4fdf858ed9 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,4 +97,4 @@ from .operations.incorrect_exp import IncorrectOperatorExponentiation from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb -from .oracles.spot_price import SpotPriceDetector +from .oracles.spot_price import SpotPriceDetector \ No newline at end of file