From ed1fa8e2e6987106cc391bc3c2efe35773836d1e Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Mon, 3 Aug 2026 20:51:07 +0530 Subject: [PATCH 1/6] added RNM --- slither/tools/mutator/mutators/all_mutators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index e2ef20a4d3..fbea03fc21 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -13,3 +13,4 @@ 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 +from slither.tools.mutator.mutators.RNM import RNM # severity medium From 15dfb3431fcd2c6dc4d499a4fedb94dfa215a726 Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Mon, 3 Aug 2026 21:50:38 +0530 Subject: [PATCH 2/6] RNM-RemoveNegationMutator --- slither/tools/mutator/mutators/RNM.py | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 slither/tools/mutator/mutators/RNM.py diff --git a/slither/tools/mutator/mutators/RNM.py b/slither/tools/mutator/mutators/RNM.py new file mode 100644 index 0000000000..8e3e850d6c --- /dev/null +++ b/slither/tools/mutator/mutators/RNM.py @@ -0,0 +1,58 @@ +from slither.core.expressions.unary_operation import UnaryOperation, UnaryOperationType +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator +from slither.tools.mutator.utils.patch import create_patch_with_line +from slither.visitors.expression.expression import ExpressionVisitor + + +class RemoveNegationVisitor(ExpressionVisitor): + def __init__(self, expression, mutator, result): + self._mutator = mutator + self._result = result + + # Traverse the expression AST + super().__init__(expression) + + def _post_unary_operation(self, expression: UnaryOperation): + # Mutate only logical negation (!) + if expression.type != UnaryOperationType.BANG: + return + + if not expression.source_mapping: + return + + operand = expression.expression + + # Replace '!expr' with 'expr' + create_patch_with_line( + self._result, + self._mutator.in_file, + expression.source_mapping.start, + expression.source_mapping.start + expression.source_mapping.length, + expression.source_mapping.content, + operand.source_mapping.content, + expression.source_mapping.lines[0], + ) + + +class RNM(AbstractMutator): + NAME = "RNM" + HELP = "Remove Negation" + + def _mutate(self) -> dict: + result = {} + + 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 + + expression = getattr(node, "expression", None) + if expression is None: + continue + + RemoveNegationVisitor(expression, self, result) + + return result \ No newline at end of file From ec572705f4d603151f34ec904ae3bb8101efa2a7 Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Mon, 3 Aug 2026 21:51:24 +0530 Subject: [PATCH 3/6] added negation functions --- .../tools/mutator/test_data/test_source_unit/src/Counter.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/tools/mutator/test_data/test_source_unit/src/Counter.sol b/tests/tools/mutator/test_data/test_source_unit/src/Counter.sol index 8ffeb9b77f..626cc55d6e 100644 --- a/tests/tools/mutator/test_data/test_source_unit/src/Counter.sol +++ b/tests/tools/mutator/test_data/test_source_unit/src/Counter.sol @@ -21,4 +21,8 @@ contract Counter { function restrictedIncrement() public onlyOwner { number++; } + + function checkNegation(uint256 newNumber) public pure { + assert(!(newNumber != 7)); + } } From ea06aead69382a11cc54f090b9d70944b928e0a3 Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Mon, 3 Aug 2026 21:52:03 +0530 Subject: [PATCH 4/6] added test for RNM --- tests/tools/mutator/test_mutator.py | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/tools/mutator/test_mutator.py b/tests/tools/mutator/test_mutator.py index 6f9af26999..afeac42ea7 100644 --- a/tests/tools/mutator/test_mutator.py +++ b/tests/tools/mutator/test_mutator.py @@ -14,6 +14,7 @@ from slither.tools.mutator.utils.file_handling import get_sol_file_list, backup_source_file from slither.utils.function import get_function_id from slither.tools.mutator.mutators.RR import RR +from slither.tools.mutator.mutators.RNM import RNM TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" @@ -283,3 +284,38 @@ def test_should_mutate_function_includes_modifier(solc_binary_path): for mod in contract.modifiers: if mod.name == "onlyOwner": assert mutator.should_mutate_function(mod) is True + + +def test_rnm_mutates_negation(solc_binary_path): + """Negations used by target function should be mutated """ + solc_path = solc_binary_path("0.8.15") + file_path = (TEST_DATA_DIR / "test_source_unit" / "src" / "Counter.sol").as_posix() + sl = Slither(file_path, solc=solc_path, compile_force_framework="solc") + + contract = next(c for c in sl.contracts if c.name == "Counter") + + with tempfile.TemporaryDirectory() as tmpdir: + mutator = RNM( + sl.compilation_units[0], + timeout=30, + testing_command="true", + testing_directory=None, + contract_instance=contract, + solc_remappings=None, + verbose=False, + output_folder=Path(tmpdir), + dont_mutate_line=[], + target_selectors=None, + target_modifiers=None, + ) + + patches = mutator._mutate() + + assert "patches" in patches + assert file_path in patches["patches"] + + assert any( + patch["old_string"] == "!(newNumber != 7)" + and patch["new_string"] == "(newNumber != 7)" + for patch in patches["patches"][file_path] + ) \ No newline at end of file From 4287e49b50b55ce4e5a45e5311233be96a7f0c56 Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Tue, 4 Aug 2026 11:55:12 +0530 Subject: [PATCH 5/6] Fix ruff --- slither/tools/mutator/mutators/RNM.py | 2 +- tests/tools/mutator/test_mutator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/slither/tools/mutator/mutators/RNM.py b/slither/tools/mutator/mutators/RNM.py index 8e3e850d6c..90fefce8ee 100644 --- a/slither/tools/mutator/mutators/RNM.py +++ b/slither/tools/mutator/mutators/RNM.py @@ -55,4 +55,4 @@ def _mutate(self) -> dict: RemoveNegationVisitor(expression, self, result) - return result \ No newline at end of file + return result diff --git a/tests/tools/mutator/test_mutator.py b/tests/tools/mutator/test_mutator.py index afeac42ea7..e3442b7182 100644 --- a/tests/tools/mutator/test_mutator.py +++ b/tests/tools/mutator/test_mutator.py @@ -318,4 +318,4 @@ def test_rnm_mutates_negation(solc_binary_path): patch["old_string"] == "!(newNumber != 7)" and patch["new_string"] == "(newNumber != 7)" for patch in patches["patches"][file_path] - ) \ No newline at end of file + ) From 769a0efe0216487827a28224ccfb5aa5794b2612 Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Tue, 4 Aug 2026 13:03:31 +0530 Subject: [PATCH 6/6] Format with Ruff --- tests/tools/mutator/test_mutator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/tools/mutator/test_mutator.py b/tests/tools/mutator/test_mutator.py index e3442b7182..99ca0de125 100644 --- a/tests/tools/mutator/test_mutator.py +++ b/tests/tools/mutator/test_mutator.py @@ -287,7 +287,7 @@ def test_should_mutate_function_includes_modifier(solc_binary_path): def test_rnm_mutates_negation(solc_binary_path): - """Negations used by target function should be mutated """ + """Negations used by target function should be mutated""" solc_path = solc_binary_path("0.8.15") file_path = (TEST_DATA_DIR / "test_source_unit" / "src" / "Counter.sol").as_posix() sl = Slither(file_path, solc=solc_path, compile_force_framework="solc") @@ -315,7 +315,6 @@ def test_rnm_mutates_negation(solc_binary_path): assert file_path in patches["patches"] assert any( - patch["old_string"] == "!(newNumber != 7)" - and patch["new_string"] == "(newNumber != 7)" + patch["old_string"] == "!(newNumber != 7)" and patch["new_string"] == "(newNumber != 7)" for patch in patches["patches"][file_path] )