Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/src/tools/Mutator.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ options:
mutant generators to run
--contract-names CONTRACT_NAMES
list of contract names you want to mutate
--target-functions TARGET_FUNCTIONS
Comma-separated list of function selectors (hex like
0xa9059cbb or signature like transfer(address,uint256))
--comprehensive continue testing minor mutations if severe mutants are uncaught
```
99 changes: 98 additions & 1 deletion slither/tools/mutator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
import time
from pathlib import Path
from typing import Type, List, Any, Optional, Union
from typing import Type, List, Any, Optional, Union, Set
from crytic_compile import cryticparser
from slither import Slither
from slither.tools.mutator.utils.testing_generated_mutant import run_test_cmd
Expand Down Expand Up @@ -89,6 +89,12 @@
help="list of contract names you want to mutate",
)

# target specific functions by selector
parser.add_argument(
"--target-functions",
help="Comma-separated list of function selectors (hex like 0xa9059cbb or signature like transfer(address,uint256))",
)

# flag to run full mutation based revert mutator output
parser.add_argument(
"--comprehensive",
Expand Down Expand Up @@ -131,6 +137,62 @@
parser.exit()


# endregion
###################################################################################
###################################################################################
# region Selector Parsing
###################################################################################
###################################################################################


def parse_target_selectors(selector_str: str) -> Set[int]:
"""Parse comma-separated selectors (hex or signature format)

Handles signatures with commas like transfer(address,uint256) by
only splitting on commas outside of parentheses.
"""
from slither.utils.function import get_function_id

Check warning on line 154 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0415: Import outside toplevel (slither.utils.function.get_function_id) (import-outside-toplevel)

selectors: Set[int] = set()

# Split on commas only when not inside parentheses
parts = []
current = ""
depth = 0
for char in selector_str:
if char == "(":
depth += 1
current += char
elif char == ")":
depth -= 1
current += char
elif char == "," and depth == 0:
parts.append(current.strip())
current = ""
else:
current += char
if current.strip():
parts.append(current.strip())

for s in parts:
if not s:
continue
if s.startswith("0x"):
# Hex format: 0xa9059cbb
if len(s) != 10:
logger.error(f"Invalid selector format: {s} (must be 0x + 8 hex chars)")
sys.exit(1)
try:
selectors.add(int(s, 16))
except ValueError:
logger.error(f"Invalid hex selector: {s}")
sys.exit(1)
else:
# Signature format: transfer(address,uint256)
selectors.add(get_function_id(s))
return selectors


# endregion
###################################################################################
###################################################################################
Expand Down Expand Up @@ -164,6 +226,10 @@
if args.contract_names:
contract_names = args.contract_names.split(",")

target_selectors: Optional[Set[int]] = None
if args.target_functions:
target_selectors = parse_target_selectors(args.target_functions)

# get all the contracts as a list from given codebase
sol_file_list: List[str] = get_sol_file_list(Path(args.codebase), paths_to_ignore_list)

Expand Down Expand Up @@ -272,6 +338,35 @@
# Add our target to the mutation list
mutated_contracts.append(target_contract.name)
logger.info(blue(f"Mutating contract {target_contract}"))

# Validate target selectors and collect target modifiers
target_modifiers: Optional[Set[str]] = None
if target_selectors:
from slither.utils.function import get_function_id

Check warning on line 345 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0415: Import outside toplevel (slither.utils.function.get_function_id) (import-outside-toplevel)

matching_functions = []
target_modifiers = set()

for func in target_contract.functions_declared:
func_selector = get_function_id(func.solidity_signature)
if func_selector in target_selectors:
matching_functions.append(func)
# Collect modifiers used by this function
for mod in func.modifiers:
target_modifiers.add(mod.name)

if not matching_functions:
logger.error(
f"No functions in {target_contract.name} match selectors: {[hex(s) for s in target_selectors]}"
)
sys.exit(1)

logger.info(
blue(f"Targeting functions: {[f.name for f in matching_functions]}")
)
if target_modifiers:
logger.info(blue(f"Including modifiers: {list(target_modifiers)}"))

for M in mutators_list:
m = M(
compilation_unit_of_main_file,
Expand All @@ -283,6 +378,8 @@
verbose,
output_folder,
dont_mutate_lines,
target_selectors=target_selectors,
target_modifiers=target_modifiers,
)
(total_counts, uncaught_counts, lines_list) = m.mutate()

Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/AOR.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/ASOR.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/BOR.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/CR.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/FHR.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def _mutate(self) -> Dict:
result: Dict = {}

for function in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
start = function.source_mapping.start
stop = start + function.source_mapping.content.find("{")
old_str = function.source_mapping.content
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/LIR.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for variable in function.local_variables:
if variable.initialized and isinstance(variable.expression, Literal):
if isinstance(variable.type, ElementaryType):
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/LOR.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/MIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/MVIE.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def _mutate(self) -> Dict:
)

for function in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for variable in function.local_variables:
if variable.initialized and not isinstance(variable.expression, Literal):
# Get the string
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/MVIV.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def _mutate(self) -> Dict:
)

for function in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for variable in function.local_variables:
if variable.initialized and isinstance(variable.expression, Literal):
start = variable.source_mapping.start
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/MWA.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ def _mutate(self) -> Dict:
result: Dict = {}

for function in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/ROR.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/RR.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def _mutate(self) -> Dict:
result: Dict = {}

for function in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/SBR.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
2 changes: 2 additions & 0 deletions slither/tools/mutator/mutators/UOR.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def _mutate(self) -> Dict:
for ( # pylint: disable=too-many-nested-blocks
function
) in self.contract.functions_and_modifiers_declared:
if not self.should_mutate_function(function):
continue
for node in function.nodes:
if not self.should_mutate_node(node):
continue
Expand Down
25 changes: 24 additions & 1 deletion slither/tools/mutator/mutators/abstract_mutator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import abc
import logging
from pathlib import Path
from typing import Optional, Dict, Tuple, List, Union
from typing import Optional, Dict, Tuple, List, Union, Set
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
Expand Down Expand Up @@ -34,6 +34,8 @@
dont_mutate_line: List[int],
rate: int = 10,
seed: Optional[int] = None,
target_selectors: Optional[Set[int]] = None,
target_modifiers: Optional[Set[str]] = None,
) -> None:
self.compilation_unit = compilation_unit
self.slither = compilation_unit.core
Expand All @@ -48,6 +50,8 @@
self.contract = contract_instance
self.in_file = self.contract.source_mapping.filename.absolute
self.dont_mutate_line = dont_mutate_line
self.target_selectors = target_selectors
self.target_modifiers = target_modifiers
# total revert/comment/tweak mutants that were generated and compiled
self.total_mutant_counts = [0, 0, 0]
# total uncaught revert/comment/tweak mutants
Expand All @@ -74,6 +78,25 @@
and node.source_mapping.filename.absolute == self.in_file
)

def should_mutate_function(self, function) -> bool:
"""Check if function/modifier should be mutated based on target_selectors"""
if self.target_selectors is None:
return True # No filter, mutate all

from slither.utils.function import get_function_id

Check warning on line 86 in slither/tools/mutator/mutators/abstract_mutator.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0415: Import outside toplevel (slither.utils.function.get_function_id) (import-outside-toplevel)
from slither.core.declarations import Modifier

Check warning on line 87 in slither/tools/mutator/mutators/abstract_mutator.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0415: Import outside toplevel (slither.core.declarations.Modifier) (import-outside-toplevel)

if isinstance(function, Modifier):
# For modifiers, check if in target_modifiers set
return bool(self.target_modifiers and function.name in self.target_modifiers)

# For functions, check selector
try:
func_selector = get_function_id(function.solidity_signature)
return func_selector in self.target_selectors
except Exception: # pylint: disable=broad-except
return False

@abc.abstractmethod
def _mutate(self) -> Dict:
"""Abstract placeholder, will be overwritten by each mutator"""
Expand Down
10 changes: 10 additions & 0 deletions tests/tools/mutator/test_data/test_source_unit/src/Counter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ pragma solidity ^0.8.15;

contract Counter {
uint256 public number;
address public owner;

modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}

function setNumber(uint256 newNumber) public {
number = newNumber;
Expand All @@ -11,4 +17,8 @@ contract Counter {
function increment() public {
number++;
}

function restrictedIncrement() public onlyOwner {
number++;
}
}
Loading
Loading