Merge branch 'crytic:dev' into dev

pull/2305/head
Tigran Avagyan 9 months ago committed by GitHub
commit 044c6beb1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/black.yml
  2. 6
      .github/workflows/ci.yml
  3. 8
      .github/workflows/docs.yml
  4. 2
      .github/workflows/doctor.yml
  5. 2
      .github/workflows/linter.yml
  6. 2
      .github/workflows/pip-audit.yml
  7. 8
      .github/workflows/publish.yml
  8. 2
      .github/workflows/pylint.yml
  9. 6
      .github/workflows/test.yml
  10. 2
      Dockerfile
  11. 12
      README.md
  12. 8
      setup.py
  13. 0
      slither/core/children/__init__.py
  14. 0
      slither/core/children/child_event.py
  15. 13
      slither/core/declarations/function.py
  16. 4
      slither/detectors/assembly/incorrect_return.py
  17. 2
      slither/detectors/assembly/return_instead_of_leave.py
  18. 2
      slither/detectors/functions/suicidal.py
  19. 2
      slither/detectors/statements/divide_before_multiply.py
  20. 2
      slither/detectors/variables/predeclaration_usage_local.py
  21. 23
      slither/slithir/convert.py
  22. 12
      slither/solc_parsing/declarations/function.py
  23. 33
      slither/tools/mutator/README.md
  24. 204
      slither/tools/mutator/__main__.py
  25. 54
      slither/tools/mutator/mutators/AOR.py
  26. 65
      slither/tools/mutator/mutators/ASOR.py
  27. 48
      slither/tools/mutator/mutators/BOR.py
  28. 39
      slither/tools/mutator/mutators/CR.py
  29. 42
      slither/tools/mutator/mutators/FHR.py
  30. 86
      slither/tools/mutator/mutators/LIR.py
  31. 46
      slither/tools/mutator/mutators/LOR.py
  32. 64
      slither/tools/mutator/mutators/MIA.py
  33. 68
      slither/tools/mutator/mutators/MVIE.py
  34. 68
      slither/tools/mutator/mutators/MVIV.py
  35. 35
      slither/tools/mutator/mutators/MWA.py
  36. 53
      slither/tools/mutator/mutators/ROR.py
  37. 38
      slither/tools/mutator/mutators/RR.py
  38. 109
      slither/tools/mutator/mutators/SBR.py
  39. 88
      slither/tools/mutator/mutators/UOR.py
  40. 126
      slither/tools/mutator/mutators/abstract_mutator.py
  41. 18
      slither/tools/mutator/mutators/all_mutators.py
  42. 15
      slither/tools/mutator/utils/command_line.py
  43. 130
      slither/tools/mutator/utils/file_handling.py
  44. 36
      slither/tools/mutator/utils/generic_patching.py
  45. 29
      slither/tools/mutator/utils/patch.py
  46. 100
      slither/tools/mutator/utils/testing_generated_mutant.py
  47. 3
      slither/tools/read_storage/__main__.py
  48. 6
      tests/e2e/compilation/test_resolution.py
  49. 2
      tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt
  50. 8
      tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol
  51. BIN
      tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol-0.7.6.zip
  52. 1
      tests/e2e/solc_parsing/test_ast_parsing.py
  53. BIN
      tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip
  54. 8
      tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json
  55. 13
      tests/e2e/solc_parsing/test_data/using-for-this-contract.sol
  56. 9
      tests/unit/core/test_function_declaration.py

@ -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

@ -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

@ -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

@ -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 }}

@ -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

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

@ -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

@ -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

@ -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

@ -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

@ -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/)
> > <sub><i>- Discussions and Support </i></sub>
@ -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

@ -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",

@ -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
###################################################################################

@ -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."

@ -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."

@ -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

@ -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

@ -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) {

@ -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

@ -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:

@ -0,0 +1,33 @@
# Slither-mutate
`slither-mutate` is a mutation testing tool for solidity based smart contracts.
## Usage
`slither-mutate <codebase> --test-cmd <test-command> <options>`
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
```

@ -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 <codebase> --test-cmd <test command> <options>",
)
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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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,
)

@ -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

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

@ -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

@ -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,
)

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

@ -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

@ -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

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

@ -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

@ -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);
}
}

@ -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"]),

@ -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"
}
}

@ -0,0 +1,13 @@
library Lib {
function f(Hello h) external {
}
}
contract Hello {
using Lib for Hello;
function test() external {
this.f();
}
}

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

Loading…
Cancel
Save