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 fetch-depth: 0
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: 3.8 python-version: 3.8

@ -55,7 +55,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- name: Install dependencies - name: Install dependencies
@ -67,11 +67,11 @@ jobs:
- name: Set up nix - name: Set up nix
if: matrix.type == 'dapp' if: matrix.type == 'dapp'
uses: cachix/install-nix-action@v23 uses: cachix/install-nix-action@v25
- name: Set up cachix - name: Set up cachix
if: matrix.type == 'dapp' if: matrix.type == 'dapp'
uses: cachix/cachix-action@v12 uses: cachix/cachix-action@v14
with: with:
name: dapp name: dapp

@ -30,17 +30,17 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v3 uses: actions/configure-pages@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v5
with: with:
python-version: '3.8' python-version: '3.8'
- run: pip install -e ".[doc]" - run: pip install -e ".[doc]"
- run: pdoc -o html/ slither '!slither.tools' #TODO fix import errors on pdoc run - run: pdoc -o html/ slither '!slither.tools' #TODO fix import errors on pdoc run
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v2 uses: actions/upload-pages-artifact@v3
with: with:
# Upload the doc # Upload the doc
path: './html/' path: './html/'
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v2 uses: actions/deploy-pages@v4

@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}

@ -31,7 +31,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: 3.8 python-version: 3.8

@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Python - name: Install Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.10"

@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.x' python-version: '3.x'
@ -23,7 +23,7 @@ jobs:
python -m pip install build python -m pip install build
python -m build python -m build
- name: Upload distributions - name: Upload distributions
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: slither-dists name: slither-dists
path: dist/ path: dist/
@ -44,10 +44,10 @@ jobs:
path: dist/ path: dist/
- name: publish - name: publish
uses: pypa/gh-action-pypi-publish@v1.8.10 uses: pypa/gh-action-pypi-publish@v1.8.11
- name: sign - name: sign
uses: sigstore/gh-action-sigstore-python@v2.1.0 uses: sigstore/gh-action-sigstore-python@v2.1.1
with: with:
inputs: ./dist/*.tar.gz ./dist/*.whl inputs: ./dist/*.tar.gz ./dist/*.whl
release-signing-artifacts: true release-signing-artifacts: true

@ -29,7 +29,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: 3.8 python-version: 3.8

@ -29,7 +29,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
cache: "pip" cache: "pip"
@ -40,7 +40,7 @@ jobs:
pip install ".[test]" pip install ".[test]"
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: '16' node-version: '16'
cache: 'npm' cache: 'npm'
@ -102,7 +102,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: 3.8 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 \ 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 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 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 - 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) [![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/) > [![Slack Status](https://slack.empirehacking.nyc/badge.svg)](https://slack.empirehacking.nyc/)
> > <sub><i>- Discussions and Support </i></sub> > > <sub><i>- Discussions and Support </i></sub>
@ -46,7 +46,7 @@
* Correctly parses 99.9% of all public Solidity code * Correctly parses 99.9% of all public Solidity code
* Average execution time of less than 1 second per contract * 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) * Integrates with Github's code scanning in [CI](https://github.com/marketplace/actions/slither-action)
* Support for Vyper * Support for Vyper smart contracts
## Usage ## Usage
@ -73,14 +73,14 @@ If you're **not** going to use one of the [supported compilation frameworks](htt
### Using Pip ### Using Pip
```console ```console
pip3 install slither-analyzer python3 -m pip install slither-analyzer
``` ```
### Using Git ### Using Git
```bash ```bash
git clone https://github.com/crytic/slither.git && cd slither 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. 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 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 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 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 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 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 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 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 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( setup(
name="slither-analyzer", 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", url="https://github.com/crytic/slither",
author="Trail of Bits", author="Trail of Bits",
version="0.9.6", version="0.10.0",
packages=find_packages(), packages=find_packages(),
python_requires=">=3.8", python_requires=">=3.8",
install_requires=[ install_requires=[
"packaging", "packaging",
"prettytable>=3.3.0", "prettytable>=3.3.0",
"pycryptodome>=3.4.6", "pycryptodome>=3.4.6",
# "crytic-compile>=0.3.1,<0.4.0", "crytic-compile>=0.3.5,<0.4.0",
"crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", # "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile",
"web3>=6.0.0", "web3>=6.0.0",
"eth-abi>=4.0.0", "eth-abi>=4.0.0",
"eth-typing>=3.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 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 # 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 [ if reentrancy_modifier in [m.name for m in self.modifiers]:
m.name for m in self.modifiers
]:
return False return False
if self.visibility in ["public", "external"]: 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: if not all_entry_points:
return True 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 # endregion
################################################################################### ###################################################################################

@ -39,7 +39,9 @@ class IncorrectReturn(AbstractDetector):
IMPACT = DetectorClassification.HIGH IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM 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_TITLE = "Incorrect return in assembly"
WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution." WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution."

@ -20,7 +20,7 @@ class ReturnInsteadOfLeave(AbstractDetector):
IMPACT = DetectorClassification.HIGH IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM 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_TITLE = "Return instead of leave in assembly"
WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used." 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"]: if func.visibility not in ["public", "external"]:
return False 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): if not ("suicide(address)" in calls or "selfdestruct(address)" in calls):
return False return False

@ -133,7 +133,7 @@ def detect_divide_before_multiply(
results: List[Tuple[FunctionContract, List[Node]]] = [] results: List[Tuple[FunctionContract, List[Node]]] = []
# Loop for each function and modifier. # 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: if not function.entry_point:
continue continue

@ -36,7 +36,7 @@ class PredeclarationUsageLocal(AbstractDetector):
```solidity ```solidity
contract C { contract C {
function f(uint z) public returns (uint) { 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; uint x = 7;
if (z % 2 == 0) { if (z % 2 == 0) {

@ -630,6 +630,17 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo
if new_ir: if new_ir:
return 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): if isinstance(t, UserDefinedType):
# UserdefinedType # UserdefinedType
t_type = t.type t_type = t.type
@ -1564,6 +1575,18 @@ def convert_to_library_or_top_level(
if new_ir: if new_ir:
return 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 return None

@ -1106,11 +1106,13 @@ class FunctionSolc(CallerContextExpression):
return node return node
def _update_reachability(self, node: Node) -> None: def _update_reachability(self, node: Node) -> None:
if node.is_reachable: worklist = [node]
return while worklist:
node.set_is_reachable(True) current = worklist.pop()
for son in node.sons: # fix point
self._update_reachability(son) if not current.is_reachable:
current.set_is_reachable(True)
worklist.extend(current.sons)
def _parse_cfg(self, cfg: Dict) -> None: 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 inspect
import logging import logging
import sys 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 crytic_compile import cryticparser
from slither import Slither from slither import Slither
from slither.tools.mutator.mutators import all_mutators from slither.tools.mutator.mutators import all_mutators
from slither.utils.colors import yellow, magenta
from .mutators.abstract_mutator import AbstractMutator from .mutators.abstract_mutator import AbstractMutator
from .utils.command_line import output_mutators from .utils.command_line import output_mutators
from .utils.file_handling import (
transfer_and_delete,
backup_source_file,
get_sol_file_list,
)
logging.basicConfig() logging.basicConfig()
logger = logging.getLogger("Slither") logger = logging.getLogger("Slither-Mutate")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
################################################################################### ###################################################################################
################################################################################### ###################################################################################
# region Cli Arguments # region Cli Arguments
@ -24,12 +29,16 @@ logger.setLevel(logging.INFO)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
"""
Parse the underlying arguments for the program.
Returns: The arguments for the program.
"""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597", 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( parser.add_argument(
"--list-mutators", "--list-mutators",
@ -39,6 +48,51 @@ def parse_args() -> argparse.Namespace:
default=False, 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 # Initiate all the crytic config cli options
cryticparser.init(parser) cryticparser.init(parser)
@ -49,9 +103,18 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args() 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_ = [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 return detectors
@ -59,7 +122,7 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods
def __call__( def __call__(
self, parser: Any, *args: Any, **kwargs: Any self, parser: Any, *args: Any, **kwargs: Any
) -> None: # pylint: disable=signature-differs ) -> None: # pylint: disable=signature-differs
checks = _get_mutators() checks = _get_mutators(None)
output_mutators(checks) output_mutators(checks)
parser.exit() 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() args = parse_args()
print(args.codebase) # arguments
sl = Slither(args.codebase, **vars(args)) test_command: str = args.test_cmd
test_directory: Optional[str] = args.test_dir
for compilation_unit in sl.compilation_units: paths_to_ignore: Optional[str] = args.ignore_dirs
for M in _get_mutators(): output_dir: Optional[str] = args.output_dir
m = M(compilation_unit) timeout: Optional[int] = args.timeout
m.mutate() 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 # 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 typing import Dict
from slither.core.cfg.node import NodeType from slither.core.cfg.node import NodeType
from slither.formatters.utils.patches import create_patch from slither.tools.mutator.utils.patch import create_patch_with_line
from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass 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 class MIA(AbstractMutator): # pylint: disable=too-few-public-methods
NAME = "MIA" NAME = "MIA"
HELP = '"if" construct around statement' HELP = '"if" construct around statement'
FAULTCLASS = FaultClass.Checking
FAULTNATURE = FaultNature.Missing
def _mutate(self) -> Dict: def _mutate(self) -> Dict:
result: Dict = {} result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for contract in self.slither.contracts: for node in function.nodes:
if node.type == NodeType.IF:
for function in contract.functions_declared + list(contract.modifiers_declared): # Get the string
start = node.expression.source_mapping.start
for node in function.nodes: stop = start + node.expression.source_mapping.length
if node.type == NodeType.IF: old_str = self.in_file_str[start:stop]
# Retrieve the file line_no = node.source_mapping.lines
in_file = contract.source_mapping.filename.absolute if not line_no[0] in self.dont_mutate_line:
# Retrieve the source code # Replace the expression with true and false
in_file_str = contract.compilation_unit.core.source_code[in_file] for value in ["true", "false"]:
new_str = value
# Get the string create_patch_with_line(
start = node.source_mapping.start result,
stop = start + node.source_mapping.length self.in_file,
old_str = in_file_str[start:stop] start,
stop,
# Replace the expression with true old_str,
new_str = "true" new_str,
line_no[0],
create_patch(result, in_file, start, stop, old_str, new_str) )
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 return result

@ -1,36 +1,60 @@
from typing import Dict from typing import Dict
from slither.core.expressions import Literal from slither.core.expressions import Literal
from slither.core.variables.variable import Variable from slither.core.variables.variable import Variable
from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator
from slither.tools.mutator.utils.generic_patching import remove_assignement from slither.tools.mutator.utils.patch import create_patch_with_line
class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods
NAME = "MVIE" NAME = "MVIE"
HELP = "variable initialization using an expression" HELP = "variable initialization using an expression"
FAULTCLASS = FaultClass.Assignement
FAULTNATURE = FaultNature.Missing
def _mutate(self) -> Dict: def _mutate(self) -> Dict:
result: Dict = {} result: Dict = {}
variable: Variable 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 return result

@ -1,37 +1,59 @@
from typing import Dict from typing import Dict
from slither.core.expressions import Literal from slither.core.expressions import Literal
from slither.core.variables.variable import Variable from slither.core.variables.variable import Variable
from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator
from slither.tools.mutator.utils.generic_patching import remove_assignement from slither.tools.mutator.utils.patch import create_patch_with_line
class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods
NAME = "MVIV" NAME = "MVIV"
HELP = "variable initialization using a value" HELP = "variable initialization using a value"
FAULTCLASS = FaultClass.Assignement
FAULTNATURE = FaultNature.Missing
def _mutate(self) -> Dict: def _mutate(self) -> Dict:
result: Dict = {} result: Dict = {}
variable: Variable variable: Variable
for contract in self.slither.contracts: # Create fault for state variables declaration
for variable in self.contract.state_variables_declared:
# Create fault for state variables declaration if variable.initialized:
for variable in contract.state_variables_declared: # Cannot remove the initialization of constant variables
if variable.initialized: if variable.is_constant:
# Cannot remove the initialization of constant variables continue
if variable.is_constant:
continue if isinstance(variable.expression, Literal):
# Get the string
if isinstance(variable.expression, Literal): start = variable.source_mapping.start
remove_assignement(variable, contract, result) stop = variable.expression.source_mapping.start
old_str = self.in_file_str[start:stop]
for function in contract.functions_declared + list(contract.modifiers_declared): new_str = old_str[: old_str.find("=")]
for variable in function.local_variables: line_no = variable.node_initialization.source_mapping.lines
if variable.initialized and isinstance(variable.expression, Literal): if not line_no[0] in self.dont_mutate_line:
remove_assignement(variable, contract, result) 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 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 abc
import logging import logging
from enum import Enum from typing import Optional, Dict, Tuple, List
from typing import Optional, Dict
from slither.core.compilation_unit import SlitherCompilationUnit from slither.core.compilation_unit import SlitherCompilationUnit
from slither.formatters.utils.patches import apply_patch, create_diff 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): class IncorrectMutatorInitialization(Exception):
pass pass
class FaultClass(Enum): class AbstractMutator(
Assignement = 0 metaclass=abc.ABCMeta
Checking = 1 ): # pylint: disable=too-few-public-methods,too-many-instance-attributes
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
NAME = "" NAME = ""
HELP = "" HELP = ""
FAULTCLASS = FaultClass.Undefined VALID_MUTANTS_COUNT = 0
FAULTNATURE = FaultNature.Undefined INVALID_MUTANTS_COUNT = 0
def __init__( def __init__( # pylint: disable=too-many-arguments
self, compilation_unit: SlitherCompilationUnit, rate: int = 10, seed: Optional[int] = None 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.compilation_unit = compilation_unit
self.slither = compilation_unit.core self.slither = compilation_unit.core
self.seed = seed self.seed = seed
self.rate = rate 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: if not self.NAME:
raise IncorrectMutatorInitialization( raise IncorrectMutatorInitialization(
@ -52,16 +61,6 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-
f"HELP is not initialized {self.__class__.__name__}" 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: if rate < 0 or rate > 100:
raise IncorrectMutatorInitialization( raise IncorrectMutatorInitialization(
f"rate must be between 0 and 100 {self.__class__.__name__}" 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""" """TODO Documentation"""
return {} return {}
def mutate(self) -> None: def mutate(self) -> Tuple[int, int, List[int]]:
all_patches = self._mutate() # call _mutate function from different mutators
(all_patches) = self._mutate()
if "patches" not in all_patches: if "patches" not in all_patches:
logger.debug(f"No patches found by {self.NAME}") logger.debug("No patches found by %s", self.NAME)
return return (0, 0, self.dont_mutate_line)
for file in all_patches["patches"]: for file in all_patches["patches"]:
original_txt = self.slither.source_code[file].encode("utf8") original_txt = self.slither.source_code[file].encode("utf8")
patched_txt = original_txt
offset = 0
patches = all_patches["patches"][file] patches = all_patches["patches"][file]
patches.sort(key=lambda x: x["start"]) 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(yellow(f"Mutating {file} with {self.NAME} \n"))
logger.info(f"Impossible to generate patch; patches collisions: {patches}")
continue
for patch in patches: for patch in patches:
patched_txt, offset = apply_patch(patched_txt, patch, offset) # test the patch
diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) flag = test_patch(
if not diff: file,
logger.info(f"Impossible to generate patch; empty {patches}") patch,
print(diff) 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 # pylint: disable=unused-import
from slither.tools.mutator.mutators.MVIV import MVIV from slither.tools.mutator.mutators.MVIV import MVIV # severity low
from slither.tools.mutator.mutators.MVIE import MVIE from slither.tools.mutator.mutators.MVIE import MVIE # severity low
from slither.tools.mutator.mutators.MIA import MIA 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 typing import List, Type
from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator
from slither.utils.myprettytable import MyPrettyTable from slither.utils.myprettytable import MyPrettyTable
@ -9,15 +8,13 @@ def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None:
for detector in mutators_classes: for detector in mutators_classes:
argument = detector.NAME argument = detector.NAME
help_info = detector.HELP help_info = detector.HELP
fault_class = detector.FAULTCLASS.name mutators_list.append((argument, help_info))
fault_nature = detector.FAULTNATURE.name table = MyPrettyTable(["Num", "Name", "What it Does"])
mutators_list.append((argument, help_info, fault_class, fault_nature))
table = MyPrettyTable(["Num", "Name", "What it Does", "Fault Class", "Fault Nature"])
# Sort by class, nature, name # Sort by class
mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[3], element[0])) mutators_list = sorted(mutators_list, key=lambda element: (element[0]))
idx = 1 idx = 1
for (argument, help_info, fault_class, fault_nature) in mutators_list: for argument, help_info in mutators_list:
table.add_row([str(idx), argument, help_info, fault_class, fault_nature]) table.add_row([str(idx), argument, help_info])
idx = idx + 1 idx = idx + 1
print(table) 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 crytic_compile import cryticparser
from slither import Slither from slither import Slither
from slither.exceptions import SlitherError
from slither.tools.read_storage.read_storage import SlitherReadStorage, RpcInfo from slither.tools.read_storage.read_storage import SlitherReadStorage, RpcInfo
@ -129,6 +130,8 @@ def main() -> None:
if args.contract_name: if args.contract_name:
contracts = slither.get_contract_from_name(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: else:
contracts = slither.contracts contracts = slither.contracts

@ -57,6 +57,6 @@ def test_contract_function_parameter(solc_binary_path) -> None:
function = contract.functions[0] function = contract.functions[0]
parameters = function.parameters parameters = function.parameters
assert (parameters[0].name == 'param1') assert parameters[0].name == "param1"
assert (parameters[1].name == '') assert parameters[1].name == ""
assert (parameters[2].name == 'param3') 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 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); 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-3-0.8.0.sol", ["0.8.15"]),
Test("using-for-functions-list-4-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-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("library_event-0.8.16.sol", ["0.8.16"]),
Test("top-level-struct-0.8.0.sol", ["0.8.0"]), Test("top-level-struct-0.8.0.sol", ["0.8.0"]),
Test("yul-top-level-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 @external
@nonreentrant("lock") @nonreentrant("lock")
def withdraw_locked(): def withdraw_locked():
self.withdraw_locked_internal()
@internal
def withdraw_locked_internal():
raw_call(msg.sender, b"", value= self.balances[msg.sender]) raw_call(msg.sender, b"", value= self.balances[msg.sender])
@payable @payable
@external @external
@ -376,10 +379,14 @@ def __default__():
assert not f.is_empty assert not f.is_empty
f = functions["withdraw_locked()"] f = functions["withdraw_locked()"]
assert not f.is_reentrant assert f.is_reentrant is False
assert f.is_implemented assert f.is_implemented
assert not f.is_empty 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") var = contract.get_state_variable_from_name("balances")
assert var assert var
assert var.solidity_signature == "balances(address)" assert var.solidity_signature == "balances(address)"

Loading…
Cancel
Save