From 527166cde89d4aa32c3c761c88675d8540f5fd91 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Oct 2025 02:18:19 -0700 Subject: [PATCH 1/4] fix: array instantiation in overloaded call 2209 --- .gitignore | 3 + slither/slithir/convert.py | 57 ++++++++++--------- .../slithir/test_array_instantiation_call.py | 49 ++++++++++++++++ .../test_data/array_instantiation_call.sol | 25 ++++++++ 4 files changed, 107 insertions(+), 27 deletions(-) create mode 100644 tests/unit/slithir/test_array_instantiation_call.py create mode 100644 tests/unit/slithir/test_data/array_instantiation_call.sol diff --git a/.gitignore b/.gitignore index 083ddebe27..e28760c6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,6 @@ crytic-export/ # slither.db.json slither.db.json + +#foundry +2209 \ No newline at end of file diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 8951707fff..2948172d71 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -225,42 +225,45 @@ def _find_function_from_parameter( else: type_args = [get_type(arg.type)] - arg_type = arg.type - if isinstance( - arg_type, ElementaryType - ) and arg_type.type in ElementaryTypeInt + Uint + Byte + ["string"]: - if isinstance(arg, Constant): - value = arg.value - can_be_uint = True - can_be_int = True - else: - value = MaxValues[arg_type.type] - can_be_uint = False - can_be_int = False - if arg_type.type in ElementaryTypeInt: - can_be_int = True - elif arg_type.type in Uint: + # if an array was instantiated in the function call, we'll get a list of types + # hence we can skip the conversion check + if not isinstance(arg, (list,)): + arg_type = arg.type + if isinstance( + arg_type, ElementaryType + ) and arg_type.type in ElementaryTypeInt + Uint + Byte + ["string"]: + if isinstance(arg, Constant): + value = arg.value can_be_uint = True + can_be_int = True + else: + value = MaxValues[arg_type.type] + can_be_uint = False + can_be_int = False + if arg_type.type in ElementaryTypeInt: + can_be_int = True + elif arg_type.type in Uint: + can_be_uint = True + + if arg_type.type in ElementaryTypeInt + Uint: + type_args = _fits_under_integer(value, can_be_int, can_be_uint) + elif value is None and arg_type.type in ["bytes", "string"]: + type_args = ["bytes", "string"] + else: + type_args = _fits_under_byte(value) + if arg_type.type == "string": + type_args += ["string"] - if arg_type.type in ElementaryTypeInt + Uint: - type_args = _fits_under_integer(value, can_be_int, can_be_uint) - elif value is None and arg_type.type in ["bytes", "string"]: - type_args = ["bytes", "string"] - else: - type_args = _fits_under_byte(value) - if arg_type.type == "string": - type_args += ["string"] - - not_found = True + found = False candidates_kept: List[Function] = [] for type_arg in type_args: - if not not_found: + if found: break candidates_kept = [] for candidate in candidates: param = get_type(candidate.parameters[idx].type) if param == type_arg: - not_found = False + found = True candidates_kept.append(candidate) if len(candidates_kept) == 1 and not full_comparison: diff --git a/tests/unit/slithir/test_array_instantiation_call.py b/tests/unit/slithir/test_array_instantiation_call.py new file mode 100644 index 0000000000..9031e5fc97 --- /dev/null +++ b/tests/unit/slithir/test_array_instantiation_call.py @@ -0,0 +1,49 @@ +from pathlib import Path +from slither import Slither +from slither.core.declarations.function_contract import FunctionContract +from slither.slithir.operations import HighLevelCall, NewArray + +# Assuming this path setup is correct for the test runner environment +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" + + +def test_array_instantiation_with_call_fix(solc_binary_path) -> None: + """ + Tests the fix for array instantiation that involves structs and function calls + within the array literal (e.g., a.f([B(num), B(num + 1)])). + + The previous bug manifested as a crash or incorrect SlithIR generation during + the processing of the array literal containing complex expressions/struct initializations. + We assert that the necessary NewArray and HighLevelCall operations are correctly generated. + """ + solc_path = solc_binary_path("0.8.0") + # Load the minimal reproducible example contract + slither = Slither(Path(TEST_DATA_DIR, "array_instantiation_call.sol").as_posix(), solc=solc_path) + + contract_c = slither.get_contract_from_name("C")[0] + + # Target the problematic function: e(A a, uint256 num) which has the struct array instantiation + # Signature matching uses addresses for contract arguments + target_func: FunctionContract = contract_c.get_function_from_signature("e(address,uint256)") + + assert target_func is not None, "Target function e(address,uint256) not found." + + # The fix is validated by ensuring the crucial SlithIR instructions exist, + # proving the parsing was successful and complete for the complex expression. + + # 1. Check for the HighLevelCall (the external call to a.f) + call_to_f = next( + (op for op in target_func.slithir_operations if isinstance(op, HighLevelCall)), + None + ) + assert call_to_f is not None, "HighLevelCall to 'a.f' should exist." + + # 2. Check for the NewArray operation, which constructs the array literal + new_array_op = next( + (op for op in target_func.slithir_operations if isinstance(op, NewArray)), + None + ) + assert new_array_op is not None, "NewArray operation must be generated for the array literal." + + # 3. Final structural check: ensure the function parsed fully without errors. + assert len(target_func.slithir_operations) > 5, "SlithIR operations are too few, indicating incomplete parsing/generation." diff --git a/tests/unit/slithir/test_data/array_instantiation_call.sol b/tests/unit/slithir/test_data/array_instantiation_call.sol new file mode 100644 index 0000000000..16ca635bac --- /dev/null +++ b/tests/unit/slithir/test_data/array_instantiation_call.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; +struct B{ + uint256 x; +} +contract A { + function f(uint256[2] calldata arr) external {} + function f(B[2] calldata arr) external {} + function f(uint256[4] calldata arr) external {} +} + +contract C { + function g(A a, uint256 num) public { + a.f([0, num]); + } + + function e(A a, uint256[2] calldata numArr) public { + a.f(numArr); + } + + function e(A a, uint256 num) public { + a.f([B(num), B(num + 1 )]); + } + +} \ No newline at end of file From b902653aaa0e70ef5992111a2019ab83818fd035 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Oct 2025 02:55:45 -0700 Subject: [PATCH 2/4] refined tests --- .../slithir/test_array_instantiation_call.py | 69 +++++++++++-------- .../test_data/array_instantiation_call.sol | 17 +++-- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/tests/unit/slithir/test_array_instantiation_call.py b/tests/unit/slithir/test_array_instantiation_call.py index 9031e5fc97..28beb56aba 100644 --- a/tests/unit/slithir/test_array_instantiation_call.py +++ b/tests/unit/slithir/test_array_instantiation_call.py @@ -1,49 +1,64 @@ from pathlib import Path from slither import Slither from slither.core.declarations.function_contract import FunctionContract -from slither.slithir.operations import HighLevelCall, NewArray +from slither.slithir.operations import HighLevelCall, NewStructure, Binary # Assuming this path setup is correct for the test runner environment +# This path points to the directory containing array_instantiation_call.sol TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" -def test_array_instantiation_with_call_fix(solc_binary_path) -> None: +def _check_common_assertions(target_func: FunctionContract, expected_description: str): + """Utility to check the base requirement: successful HighLevelCall generation.""" + assert target_func is not None, f"Target function for {expected_description} not found." + + slithir_ops = target_func.slithir_operations + + # 1. Check for the HighLevelCall (the external call to a.f) + # The presence of this call proves the array and argument resolution completed without crashing. + call_to_f = next( + (op for op in slithir_ops if isinstance(op, HighLevelCall)), + None + ) + assert call_to_f is not None, f"[{expected_description}]: HighLevelCall to 'a.f' should exist." + assert len(slithir_ops) > 0, f"[{expected_description}]: No SlithIR operations found, indicating a crash or severe parsing failure." + + +def test_primitive_array_instantiation_fix(solc_binary_path) -> None: + """ + Tests the fix for array instantiation of simple primitive types (e.g., a.f([0, num])). + This case was previously bugged and must now successfully generate the HighLevelCall. """ - Tests the fix for array instantiation that involves structs and function calls - within the array literal (e.g., a.f([B(num), B(num + 1)])). + solc_path = solc_binary_path("0.8.0") + slither = Slither(Path(TEST_DATA_DIR, "array_instantiation_call.sol").as_posix(), solc=solc_path) + contract_c = slither.get_contract_from_name("C")[0] + + target_func: FunctionContract = contract_c.get_function_from_signature("test_primitive_array_instantiation(address,uint256)") + + _check_common_assertions(target_func, "Primitive Array Instantiation") + + - The previous bug manifested as a crash or incorrect SlithIR generation during - the processing of the array literal containing complex expressions/struct initializations. - We assert that the necessary NewArray and HighLevelCall operations are correctly generated. +def test_struct_array_instantiation_fix(solc_binary_path) -> None: + """ + Tests the fix for complex array instantiation involving structs and arithmetic + (e.g., a.f([B(num), B(num + 1)])). This was a more complex buggy case. """ solc_path = solc_binary_path("0.8.0") - # Load the minimal reproducible example contract slither = Slither(Path(TEST_DATA_DIR, "array_instantiation_call.sol").as_posix(), solc=solc_path) contract_c = slither.get_contract_from_name("C")[0] - # Target the problematic function: e(A a, uint256 num) which has the struct array instantiation - # Signature matching uses addresses for contract arguments - target_func: FunctionContract = contract_c.get_function_from_signature("e(address,uint256)") + target_func: FunctionContract = contract_c.get_function_from_signature("test_struct_array_instantiation(address,uint256)") - assert target_func is not None, "Target function e(address,uint256) not found." + _check_common_assertions(target_func, "Struct Array Instantiation") - # The fix is validated by ensuring the crucial SlithIR instructions exist, - # proving the parsing was successful and complete for the complex expression. + slithir_ops = target_func.slithir_operations - # 1. Check for the HighLevelCall (the external call to a.f) - call_to_f = next( - (op for op in target_func.slithir_operations if isinstance(op, HighLevelCall)), - None - ) - assert call_to_f is not None, "HighLevelCall to 'a.f' should exist." - # 2. Check for the NewArray operation, which constructs the array literal - new_array_op = next( - (op for op in target_func.slithir_operations if isinstance(op, NewArray)), + #Check for the NewStructure operation (for B(num) or B(num + 1)) + new_struct_op = next( + (op for op in slithir_ops if isinstance(op, NewStructure)), None ) - assert new_array_op is not None, "NewArray operation must be generated for the array literal." - - # 3. Final structural check: ensure the function parsed fully without errors. - assert len(target_func.slithir_operations) > 5, "SlithIR operations are too few, indicating incomplete parsing/generation." + assert new_struct_op is not None, "[Struct Array Instantiation]: NewStructure operation must be generated for struct B creation." diff --git a/tests/unit/slithir/test_data/array_instantiation_call.sol b/tests/unit/slithir/test_data/array_instantiation_call.sol index 16ca635bac..d669903426 100644 --- a/tests/unit/slithir/test_data/array_instantiation_call.sol +++ b/tests/unit/slithir/test_data/array_instantiation_call.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; + struct B{ uint256 x; } + contract A { function f(uint256[2] calldata arr) external {} function f(B[2] calldata arr) external {} @@ -10,16 +12,17 @@ contract A { } contract C { - function g(A a, uint256 num) public { + // TEST CASE 1: Simple array literal of primitives. + // This case was also bugged by the array instantiation issue. + function test_primitive_array_instantiation(A a, uint256 num) public { a.f([0, num]); } - function e(A a, uint256[2] calldata numArr) public { - a.f(numArr); - } - - function e(A a, uint256 num) public { + // TEST CASE 2: Complex array literal of structs with arithmetic. + // This is the original complex bug case. + function test_struct_array_instantiation(A a, uint256 num) public { a.f([B(num), B(num + 1 )]); } -} \ No newline at end of file + +} From 5d053f2e01ea038c44f2f8eb4dbb49cff96ffd24 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Oct 2025 03:03:59 -0700 Subject: [PATCH 3/4] more useful comments on fix --- slither/slithir/convert.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 2948172d71..6e986cd368 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -219,14 +219,18 @@ def _find_function_from_parameter( type_args: List[str] for idx, arg in enumerate(arguments): if isinstance(arg, (list,)): + # If the argument is an array literal (list of IR variables), the type is a fixed array. + # We resolve the complete fixed array type here (e.g., 'struct B[2]') and store it in type_args. type_args = [f"{get_type(arg[0].type)}[{len(arg)}]"] elif isinstance(arg, Function): type_args = [arg.signature_str] else: type_args = [get_type(arg.type)] - # if an array was instantiated in the function call, we'll get a list of types - # hence we can skip the conversion check + # --- Array Instantiation Guard --- + # If the argument was an array literal (list), its type was definitively resolved above. + # We must skip the subsequent logic, which attempts implicit type conversions + # designed only for single ElementaryTypes (uint/int/bytes). if not isinstance(arg, (list,)): arg_type = arg.type if isinstance( From f43f8687e6fc382eb4894dd9258a1f224ff2bbea Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 18 Oct 2025 15:59:45 -0700 Subject: [PATCH 4/4] CI fixes and gitignore cleanup --- .gitignore | 5 +- .../slithir/test_array_instantiation_call.py | 61 ++++++++++--------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index e28760c6d3..c5a6a7a5ab 100644 --- a/.gitignore +++ b/.gitignore @@ -114,7 +114,4 @@ test_artifacts/ crytic-export/ # slither.db.json -slither.db.json - -#foundry -2209 \ No newline at end of file +slither.db.json \ No newline at end of file diff --git a/tests/unit/slithir/test_array_instantiation_call.py b/tests/unit/slithir/test_array_instantiation_call.py index 28beb56aba..2abb317562 100644 --- a/tests/unit/slithir/test_array_instantiation_call.py +++ b/tests/unit/slithir/test_array_instantiation_call.py @@ -1,27 +1,25 @@ from pathlib import Path from slither import Slither from slither.core.declarations.function_contract import FunctionContract -from slither.slithir.operations import HighLevelCall, NewStructure, Binary +from slither.slithir.operations import HighLevelCall, NewStructure # Assuming this path setup is correct for the test runner environment # This path points to the directory containing array_instantiation_call.sol -TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" - +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" def _check_common_assertions(target_func: FunctionContract, expected_description: str): """Utility to check the base requirement: successful HighLevelCall generation.""" assert target_func is not None, f"Target function for {expected_description} not found." - + slithir_ops = target_func.slithir_operations - + # 1. Check for the HighLevelCall (the external call to a.f) # The presence of this call proves the array and argument resolution completed without crashing. - call_to_f = next( - (op for op in slithir_ops if isinstance(op, HighLevelCall)), - None - ) + call_to_f = next((op for op in slithir_ops if isinstance(op, HighLevelCall)), None) assert call_to_f is not None, f"[{expected_description}]: HighLevelCall to 'a.f' should exist." - assert len(slithir_ops) > 0, f"[{expected_description}]: No SlithIR operations found, indicating a crash or severe parsing failure." + assert ( + len(slithir_ops) > 0 + ), f"[{expected_description}]: No SlithIR operations found, indicating a crash or severe parsing failure." def test_primitive_array_instantiation_fix(solc_binary_path) -> None: @@ -30,35 +28,40 @@ def test_primitive_array_instantiation_fix(solc_binary_path) -> None: This case was previously bugged and must now successfully generate the HighLevelCall. """ solc_path = solc_binary_path("0.8.0") - slither = Slither(Path(TEST_DATA_DIR, "array_instantiation_call.sol").as_posix(), solc=solc_path) + slither = Slither( + Path(TEST_DATA_DIR, "array_instantiation_call.sol").as_posix(), solc=solc_path + ) contract_c = slither.get_contract_from_name("C")[0] - - target_func: FunctionContract = contract_c.get_function_from_signature("test_primitive_array_instantiation(address,uint256)") - + + target_func: FunctionContract = contract_c.get_function_from_signature( + "test_primitive_array_instantiation(address,uint256)" + ) + _check_common_assertions(target_func, "Primitive Array Instantiation") - def test_struct_array_instantiation_fix(solc_binary_path) -> None: """ - Tests the fix for complex array instantiation involving structs and arithmetic + Tests the fix for complex array instantiation involving structs and arithmetic (e.g., a.f([B(num), B(num + 1)])). This was a more complex buggy case. """ solc_path = solc_binary_path("0.8.0") - slither = Slither(Path(TEST_DATA_DIR, "array_instantiation_call.sol").as_posix(), solc=solc_path) + slither = Slither( + Path(TEST_DATA_DIR, "array_instantiation_call.sol").as_posix(), solc=solc_path + ) contract_c = slither.get_contract_from_name("C")[0] - - target_func: FunctionContract = contract_c.get_function_from_signature("test_struct_array_instantiation(address,uint256)") - + + target_func: FunctionContract = contract_c.get_function_from_signature( + "test_struct_array_instantiation(address,uint256)" + ) + _check_common_assertions(target_func, "Struct Array Instantiation") - + slithir_ops = target_func.slithir_operations - - - #Check for the NewStructure operation (for B(num) or B(num + 1)) - new_struct_op = next( - (op for op in slithir_ops if isinstance(op, NewStructure)), - None - ) - assert new_struct_op is not None, "[Struct Array Instantiation]: NewStructure operation must be generated for struct B creation." + + # Check for the NewStructure operation (for B(num) or B(num + 1)) + new_struct_op = next((op for op in slithir_ops if isinstance(op, NewStructure)), None) + assert ( + new_struct_op is not None + ), "[Struct Array Instantiation]: NewStructure operation must be generated for struct B creation."