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)"