mirror of https://github.com/crytic/slither
commit
044c6beb1c
@ -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 |
||||||
|
``` |
@ -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,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 |
||||||
|
@ -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 |
@ -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 |
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
Loading…
Reference in new issue