From 3b6a3c44bc73195a63970f814539323b2c1093eb Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Fri, 3 Apr 2026 07:40:16 +0530 Subject: [PATCH 1/3] Add ACN mutator --- slither/tools/mutator/mutators/ACN.py | 63 +++++++++++++++++++ .../tools/mutator/mutators/all_mutators.py | 1 + 2 files changed, 64 insertions(+) create mode 100644 slither/tools/mutator/mutators/ACN.py diff --git a/slither/tools/mutator/mutators/ACN.py b/slither/tools/mutator/mutators/ACN.py new file mode 100644 index 0000000000..8af9816b0d --- /dev/null +++ b/slither/tools/mutator/mutators/ACN.py @@ -0,0 +1,63 @@ +from slither.core.declarations.solidity_variables import SolidityFunction +from slither.core.expressions.call_expression import CallExpression +from slither.core.expressions.identifier import Identifier +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 + + +class ACN(AbstractMutator): + NAME = "ACN" + HELP = "Assert Condition Negation" + + 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 + + try: + expression = node.expression + except AttributeError: + continue + + if not isinstance(expression, CallExpression): + continue + + if not isinstance(expression.called, Identifier): + continue + + called = expression.called.value + if not isinstance(called, SolidityFunction): + continue + + if not called.name.startswith("assert("): + continue + + if not expression.arguments: + continue + + condition = expression.arguments[0] + + start = condition.source_mapping.start + stop = start + condition.source_mapping.length + old_str = condition.source_mapping.content + line_no = condition.source_mapping.lines[0] + new_str = f"!({old_str})" + + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no, + ) + + return result diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index e2ef20a4d3..3e47280b36 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -11,5 +11,6 @@ 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.ACN import ACN # severity medium from slither.tools.mutator.mutators.RR import RR # severity high from slither.tools.mutator.mutators.CR import CR # severity high From aafec61f7c537deaed90884edadfd19111a351ae Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Fri, 3 Apr 2026 07:40:28 +0530 Subject: [PATCH 2/3] Add ACN mutator tests --- .../test_source_unit/src/Counter.sol | 1 + tests/tools/mutator/test_mutator.py | 34 +++++++++++++++++++ 2 files changed, 35 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..486716b84f 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 @@ -11,6 +11,7 @@ contract Counter { } function setNumber(uint256 newNumber) public { + assert(newNumber != 7); number = newNumber; } diff --git a/tests/tools/mutator/test_mutator.py b/tests/tools/mutator/test_mutator.py index 6f9af26999..ecf8ecac5f 100644 --- a/tests/tools/mutator/test_mutator.py +++ b/tests/tools/mutator/test_mutator.py @@ -14,6 +14,8 @@ 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.ACN import ACN + TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" @@ -283,3 +285,35 @@ 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_acn_mutates_assert_condition(solc_binary_path): + 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 = ACN( + 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] + ) From 75fefba06c4fc01db893a77ef436b7af2701c676 Mon Sep 17 00:00:00 2001 From: sidarth16 Date: Fri, 3 Apr 2026 08:23:46 +0530 Subject: [PATCH 3/3] fix ruff --- tests/tools/mutator/test_mutator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/tools/mutator/test_mutator.py b/tests/tools/mutator/test_mutator.py index ecf8ecac5f..f08cc8a202 100644 --- a/tests/tools/mutator/test_mutator.py +++ b/tests/tools/mutator/test_mutator.py @@ -17,7 +17,6 @@ from slither.tools.mutator.mutators.ACN import ACN - TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" foundry_available = shutil.which("forge") is not None @@ -313,7 +312,6 @@ def test_acn_mutates_assert_condition(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] )