diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 6017255b2..082333f2f 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 f04436bd3..7972c96e7 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 @@ -67,11 +67,11 @@ jobs: - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@v23 + uses: cachix/install-nix-action@v25 - name: Set up cachix if: matrix.type == 'dapp' - uses: cachix/cachix-action@v12 + uses: cachix/cachix-action@v14 with: name: dapp diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 29356c0c6..0942afb6d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,17 +30,17 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v3 - - uses: actions/setup-python@v4 + uses: actions/configure-pages@v4 + - uses: actions/setup-python@v5 with: python-version: '3.8' - 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/' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/doctor.yml b/.github/workflows/doctor.yml index 0a0eb896d..555452871 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 5415b6d1b..45dc0d8c2 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 a98f6ab58..1c0a1d40a 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 7b4d61e89..72b002d1e 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' @@ -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/ @@ -44,10 +44,10 @@ 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 + uses: sigstore/gh-action-sigstore-python@v2.1.1 with: inputs: ./dist/*.tar.gz ./dist/*.whl release-signing-artifacts: true diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 7e990371f..091da2b96 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 cb8f0ea6e..68a32f80a 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" @@ -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' @@ -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 diff --git a/Dockerfile b/Dockerfile index d0a7d67be..6de5ec2c6 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 diff --git a/README.md b/README.md index 0f548d6b8..769ac58aa 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 @@ -46,7 +46,7 @@ * Correctly parses 99.9% of all public Solidity code * Average execution time of less than 1 second per contract * Integrates with Github's code scanning in [CI](https://github.com/marketplace/actions/slither-action) -* Support for Vyper +* Support for Vyper smart contracts ## Usage @@ -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. @@ -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/setup.py b/setup.py index 16aa80568..332f8fc18 100644 --- a/setup.py +++ b/setup.py @@ -5,18 +5,18 @@ with open("README.md", "r", encoding="utf-8") as f: setup( name="slither-analyzer", - description="Slither is a Solidity static analysis framework written in Python 3.", + description="Slither is a Solidity and Vyper static analysis framework written in Python 3.", url="https://github.com/crytic/slither", author="Trail of Bits", - version="0.9.6", + version="0.10.0", packages=find_packages(), python_requires=">=3.8", install_requires=[ "packaging", "prettytable>=3.3.0", "pycryptodome>=3.4.6", - # "crytic-compile>=0.3.1,<0.4.0", - "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", + "crytic-compile>=0.3.5,<0.4.0", + # "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", "web3>=6.0.0", "eth-abi>=4.0.0", "eth-typing>=3.0.0", diff --git a/slither/core/children/__init__.py b/slither/core/children/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/slither/core/children/child_event.py b/slither/core/children/child_event.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py index e803154d0..d2baaf7e7 100644 --- a/slither/core/declarations/function.py +++ b/slither/core/declarations/function.py @@ -1500,10 +1500,13 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu """ 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 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu ] 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/slither/detectors/assembly/incorrect_return.py b/slither/detectors/assembly/incorrect_return.py index f5f0a98d9..bd5a6d844 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 a1591d834..a1ad9c87e 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/slither/detectors/functions/suicidal.py b/slither/detectors/functions/suicidal.py index 1f8cb52f9..f0af978ec 100644 --- a/slither/detectors/functions/suicidal.py +++ b/slither/detectors/functions/suicidal.py @@ -59,7 +59,7 @@ Bob calls `kill` and destructs the contract.""" 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/slither/detectors/statements/divide_before_multiply.py b/slither/detectors/statements/divide_before_multiply.py index 334da592c..1f6ccd87e 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 diff --git a/slither/detectors/variables/predeclaration_usage_local.py b/slither/detectors/variables/predeclaration_usage_local.py index b4d75e51a..9816dd6e2 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) { diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 4411e3505..170df8cba 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/slither/solc_parsing/declarations/function.py b/slither/solc_parsing/declarations/function.py index 59940ec1c..c1b94661d 100644 --- a/slither/solc_parsing/declarations/function.py +++ b/slither/solc_parsing/declarations/function.py @@ -1106,11 +1106,13 @@ class FunctionSolc(CallerContextExpression): 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: diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md new file mode 100644 index 000000000..8af977b08 --- /dev/null +++ b/slither/tools/mutator/README.md @@ -0,0 +1,33 @@ +# Slither-mutate + +`slither-mutate` is a mutation testing tool for solidity based smart contracts. + +## Usage + +`slither-mutate --test-cmd ` + +To view the list of mutators available `slither-mutate --list-mutators` + +### CLI Interface + +```shell +positional arguments: + codebase Codebase to analyze (.sol file, project directory, ...) + +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') + --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 +``` diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 84286ce66..5c13d7aea 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -2,20 +2,25 @@ import argparse import inspect import logging import sys -from typing import Type, List, Any - +import os +import shutil +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, +) logging.basicConfig() -logger = logging.getLogger("Slither") +logger = logging.getLogger("Slither-Mutate") logger.setLevel(logging.INFO) - ################################################################################### ################################################################################### # region Cli Arguments @@ -24,12 +29,16 @@ logger.setLevel(logging.INFO) 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 target", + 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", @@ -39,6 +48,51 @@ def parse_args() -> argparse.Namespace: default=False, ) + # argument to add the test command + 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") + + # argument to ignore the interfaces, libraries + 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)") + + # output directory argument + parser.add_argument( + "--output-dir", help="Name of 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", + ) + + # 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) @@ -49,9 +103,18 @@ 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 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)] return detectors @@ -59,7 +122,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() @@ -72,17 +135,120 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods ################################################################################### -def main() -> None: - +def main() -> (None): # pylint: disable=too-many-statements,too-many-branches,too-many-locals 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() + # arguments + test_command: str = args.test_cmd + 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(",") + 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 + 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 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 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() + for M in duplicate_list: + if M.NAME == "RR": + mutators_list.remove(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: # 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 + files_dict = backup_source_file(sl.source_code, output_folder) + # total count of mutants + 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: + 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 == "": + 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, + ) + (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 + 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 + 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 new file mode 100644 index 000000000..0bf0fb2a2 --- /dev/null +++ b/slither/tools/mutator/mutators/AOR.py @@ -0,0 +1,54 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.tools.mutator.utils.patch import create_patch_with_line +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator +from slither.core.expressions.unary_operation import UnaryOperation + +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" + + def _mutate(self) -> Dict: + result: Dict = {} + 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 + 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: + # 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: + # 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/ASOR.py b/slither/tools/mutator/mutators/ASOR.py new file mode 100644 index 000000000..2ff403b38 --- /dev/null +++ b/slither/tools/mutator/mutators/ASOR.py @@ -0,0 +1,65 @@ +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, +) + +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" + + def _mutate(self) -> Dict: + result: Dict = {} + + 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 ir.expression.type == AssignmentOperationType.ASSIGN: + continue + alternative_ops = assignment_operators[:] + try: + alternative_ops.remove(ir.expression.type) + except: # pylint: disable=bare-except + continue + for op in alternative_ops: + 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 + 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 diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py new file mode 100644 index 000000000..a8720a4b6 --- /dev/null +++ b/slither/tools/mutator/mutators/BOR.py @@ -0,0 +1,48 @@ +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 + +bitwise_operators = [ + BinaryType.AND, + 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" + + def _mutate(self) -> Dict: + result: Dict = {} + + 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: + 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 + 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/CR.py b/slither/tools/mutator/mutators/CR.py new file mode 100644 index 000000000..ebf93bf18 --- /dev/null +++ b/slither/tools/mutator/mutators/CR.py @@ -0,0 +1,39 @@ +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 + + +class CR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "CR" + HELP = "Comment Replacement" + + def _mutate(self) -> Dict: + result: Dict = {} + + 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, + ): + # 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: + 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 new file mode 100644 index 000000000..028c1916c --- /dev/null +++ b/slither/tools/mutator/mutators/FHR.py @@ -0,0 +1,42 @@ +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 + + +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" + + 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("{") + 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(" ==> ", 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], + ) + return result diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py new file mode 100644 index 000000000..cc58cbae1 --- /dev/null +++ b/slither/tools/mutator/mutators/LIR.py @@ -0,0 +1,86 @@ +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 +from slither.tools.mutator.utils.patch import create_patch_with_line +from slither.core.solidity_types import ElementaryType + +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 + result: Dict = {} + variable: Variable + + # Create fault for state variables declaration + 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: + 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 + 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 ( # 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") + 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 + 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 new file mode 100644 index 000000000..2d1535b1a --- /dev/null +++ b/slither/tools/mutator/mutators/LOR.py @@ -0,0 +1,46 @@ +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 + +logical_operators = [ + BinaryType.OROR, + BinaryType.ANDAND, +] + + +class LOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "LOR" + HELP = "Logical Operator Replacement" + + def _mutate(self) -> Dict: + result: Dict = {} + + 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: + 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 = 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 + 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/MIA.py b/slither/tools/mutator/mutators/MIA.py index 405888f8b..f29569f63 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,39 +1,47 @@ 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.patch import create_patch_with_line +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' - FAULTCLASS = FaultClass.Checking - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - 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) - + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type == NodeType.IF: + # Get the string + 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 + 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], + ) return result diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index a16a8252e..ce51792ff 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,36 +1,60 @@ 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.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" - FAULTCLASS = FaultClass.Assignement - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - 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) + # 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 not isinstance(variable.expression, Literal): + # 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 = variable.node_initialization.source_mapping.lines + 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: + if variable.initialized and not isinstance(variable.expression, Literal): + # 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 = variable.source_mapping.lines + 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 d4a7c5486..f9e51c553 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,37 +1,59 @@ 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.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" - FAULTCLASS = FaultClass.Assignement - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - 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) - + # 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): + # 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 = variable.node_initialization.source_mapping.lines + 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: + if variable.initialized and isinstance(variable.expression, Literal): + 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 + 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 new file mode 100644 index 000000000..9682f10ca --- /dev/null +++ b/slither/tools/mutator/mutators/MWA.py @@ -0,0 +1,35 @@ +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 +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' + + 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 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 diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py new file mode 100644 index 000000000..9daae0663 --- /dev/null +++ b/slither/tools/mutator/mutators/ROR.py @@ -0,0 +1,53 @@ +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 + +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" + + def _mutate(self) -> Dict: + result: Dict = {} + + 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" + ): + 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 + 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 new file mode 100644 index 000000000..e285d7a3f --- /dev/null +++ b/slither/tools/mutator/mutators/RR.py @@ -0,0 +1,38 @@ +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 + + +class RR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "RR" + 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, + ): + # 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], + ) + return result diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py new file mode 100644 index 000000000..efbda4877 --- /dev/null +++ b/slither/tools/mutator/mutators/SBR.py @@ -0,0 +1,109 @@ +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 +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", + "^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", + "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 ( # 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, + ): + # 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: + for value in solidity_rules: + 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], + ) + + 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] + 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(" ==> ", 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], + ) + return result diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py new file mode 100644 index 000000000..f427c2fbf --- /dev/null +++ b/slither/tools/mutator/mutators/UOR.py @@ -0,0 +1,88 @@ +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 + +unary_operators = [ + UnaryOperationType.PLUSPLUS_PRE, + UnaryOperationType.MINUSMINUS_PRE, + UnaryOperationType.PLUSPLUS_POST, + UnaryOperationType.MINUSMINUS_POST, + UnaryOperationType.MINUS_PRE, +] + + +class UOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "UOR" + HELP = "Unary Operator Replacement" + + def _mutate(self) -> Dict: + result: Dict = {} + + 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 + 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 + ): + 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], + ) + 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], + ) + return result diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 169d8725e..375af1e6f 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,46 +1,55 @@ import abc import logging -from enum import Enum -from typing import Optional, Dict - +from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit from slither.formatters.utils.patches import apply_patch, create_diff +from slither.tools.mutator.utils.testing_generated_mutant import test_patch +from slither.utils.colors import yellow +from slither.core.declarations import Contract -logger = logging.getLogger("Slither") +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 - - -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 = "" - FAULTCLASS = FaultClass.Undefined - FAULTNATURE = FaultNature.Undefined - - def __init__( - self, compilation_unit: SlitherCompilationUnit, rate: int = 10, seed: Optional[int] = None - ): + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 + + 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, + seed: Optional[int] = None, + ) -> 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 + 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] + self.dont_mutate_line = dont_mutate_line if not self.NAME: raise IncorrectMutatorInitialization( @@ -52,16 +61,6 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- 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__}" - ) - if rate < 0 or rate > 100: raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" @@ -72,25 +71,50 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- """TODO Documentation""" return {} - def mutate(self) -> None: - all_patches = self._mutate() - + 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 + 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") - 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.info(f"Impossible to generate patch; patches collisions: {patches}") - continue + logger.info(yellow(f"Mutating {file} with {self.NAME} \n")) 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(diff) + # 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 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) + 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", 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 5508fb68e..b02a2cc9b 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,4 +1,16 @@ # 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 # 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 feb479c5c..79d705097 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -1,5 +1,4 @@ from typing import List, Type - from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.utils.myprettytable import MyPrettyTable @@ -9,15 +8,13 @@ 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)) + table = MyPrettyTable(["Num", "Name", "What it Does"]) - # Sort by class, nature, name - mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[3], element[0])) + # Sort by class + mutators_list = sorted(mutators_list, key=lambda element: (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 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 new file mode 100644 index 000000000..ddb3efb50 --- /dev/null +++ b/slither/tools/mutator/utils/file_handling.py @@ -0,0 +1,130 @@ +import os +from typing import Dict, List +import logging + +logger = logging.getLogger("Slither-Mutate") + +duplicated_files = {} + + +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", 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: + content = duplicated_file.read() + + 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: # 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: + content = source_file.read() + + # 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 + + "/" + + 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: + duplicate_content = duplicated_file.read() + + with open(file, "w", encoding="utf8") as source_file: + source_file.write(duplicate_content) + + 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: + duplicate_content = duplicated_file.read() + + with open(file, "w", encoding="utf8") as source_file: + source_file.write(duplicate_content) + + except Exception as e: # pylint: disable=broad-except + logger.error(f"Error resetting file: {e}") + + +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 is None: + ignore_paths = [] + + # if input is contract file + if os.path.isfile(codebase): + return [codebase] + + # if input is folder + if 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): + _, 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 diff --git a/slither/tools/mutator/utils/generic_patching.py b/slither/tools/mutator/utils/generic_patching.py deleted file mode 100644 index d773ea784..000000000 --- a/slither/tools/mutator/utils/generic_patching.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Dict - -from slither.core.declarations import Contract -from slither.core.variables.variable import Variable -from slither.formatters.utils.patches import create_patch - - -def remove_assignement(variable: Variable, contract: Contract, result: Dict): - """ - 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("=")] - - create_patch( - result, - in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - ) diff --git a/slither/tools/mutator/utils/patch.py b/slither/tools/mutator/utils/patch.py new file mode 100644 index 000000000..54ff81e60 --- /dev/null +++ b/slither/tools/mutator/utils/patch.py @@ -0,0 +1,29 @@ +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) 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 000000000..4c51b7e5a --- /dev/null +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -0,0 +1,100 @@ +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") + + +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=bare-except + return False + + +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" + # add --bail for hardhat and truffle tests, to exit after first failure + elif "hardhat test" in cmd or "truffle test" in cmd and "--bail" not in cmd: + cmd += " --bail" + + start = time.time() + + # starting new process + 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( # 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: + 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", 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" + ) + ) + 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" + ) + ) + return False diff --git a/slither/tools/read_storage/__main__.py b/slither/tools/read_storage/__main__.py index 8415ae185..3baa5d351 100644 --- a/slither/tools/read_storage/__main__.py +++ b/slither/tools/read_storage/__main__.py @@ -7,6 +7,7 @@ import argparse 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 diff --git a/tests/e2e/compilation/test_resolution.py b/tests/e2e/compilation/test_resolution.py index af7cbe2c7..c3290624b 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" 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 4a784217d..99a6a0295 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 428c794d4..31b22d767 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 635092d49..ecd80364b 100644 Binary files a/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol-0.7.6.zip and b/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol-0.7.6.zip differ diff --git a/tests/e2e/solc_parsing/test_ast_parsing.py b/tests/e2e/solc_parsing/test_ast_parsing.py index bc57dc51b..e233fa993 100644 --- a/tests/e2e/solc_parsing/test_ast_parsing.py +++ b/tests/e2e/solc_parsing/test_ast_parsing.py @@ -448,6 +448,7 @@ ALL_TESTS = [ 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 000000000..6950666a4 Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip differ 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 000000000..43eca2c9a --- /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 000000000..33bbc74cd --- /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(); + } +} + diff --git a/tests/unit/core/test_function_declaration.py b/tests/unit/core/test_function_declaration.py index cea207613..f75198d24 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)"