diff --git a/slither/analyses/data_dependency/data_dependency.py b/slither/analyses/data_dependency/data_dependency.py index 12e809fa53..8272590dbf 100644 --- a/slither/analyses/data_dependency/data_dependency.py +++ b/slither/analyses/data_dependency/data_dependency.py @@ -293,6 +293,74 @@ def get_all_dependencies_ssa( return context.context[KEY_SSA] +def get_must_depends_on(variable: SUPPORTED_TYPES) -> List: + """ + Return must dependency of a variable if exist otherwise return None. + + :param variable: target variable whose must dependency needs to be computed + :return: Variable | None + """ + must_dependencies = compute_must_dependencies(variable) + if len(must_dependencies) != 1: + return [] + return [list(must_dependencies)[0]] + + +def compute_must_dependencies(v: SUPPORTED_TYPES) -> Set[Variable]: + if isinstance(v, (SolidityVariableComposed, Constant)) or ( + v.function.visibility in ["public", "external"] and v in v.function.parameters + ): + return set([v]) + + function_dependencies = {} + function_dependencies["context"] = {} + lvalues = [] + + for node in v.function.nodes: + for ir in node.irs_ssa: + if isinstance(ir, OperationWithLValue) and ir.lvalue: + if isinstance(ir.lvalue, LocalIRVariable) and ir.lvalue.is_storage: + continue + if isinstance(ir.lvalue, ReferenceVariable): + lvalue = ir.lvalue.points_to + if lvalue: + lvalues.append((lvalue, v.function, ir)) + lvalues.append((ir.lvalue, v.function, ir)) + + for lvalue_details in lvalues: + lvalue = lvalue_details[0] + ir = lvalue_details[2] + + if not lvalue in function_dependencies["context"]: + function_dependencies["context"][lvalue] = set() + read: Union[List[Union[LVALUE, SolidityVariableComposed]], List[SlithIRVariable]] + + if isinstance(ir, Index): + read = [ir.variable_left] + elif isinstance(ir, InternalCall) and ir.function: + read = ir.function.return_values_ssa + else: + read = ir.read + for variable in read: + # if not isinstance(variable, Constant): + function_dependencies["context"][lvalue].add(variable) + function_dependencies["context"] = convert_to_non_ssa(function_dependencies["context"]) + + must_dependencies = set() + data_dependencies = ( + list(function_dependencies["context"][v]) + if function_dependencies["context"] is not None + else [] + ) + for i, data_dependency in enumerate(data_dependencies): + result = compute_must_dependencies(data_dependency) + if i > 0: + must_dependencies = must_dependencies.intersection(result) + else: + must_dependencies = must_dependencies.union(result) + return must_dependencies + + # endregion ################################################################################### ################################################################################### diff --git a/tests/unit/core/test_data/must_depend_on.sol b/tests/unit/core/test_data/must_depend_on.sol new file mode 100644 index 0000000000..3cc1de38f0 --- /dev/null +++ b/tests/unit/core/test_data/must_depend_on.sol @@ -0,0 +1,30 @@ +pragma solidity ^0.8.19; + +interface IERC20 { + function transferFrom(address from, address to, uint amount) external returns (bool); +} + +/** + * @title MissingReturnBug + * @author IllIllI + */ + +// test case of the missing return bug described here: +// https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca +contract Unsafe { + IERC20 erc20; + function good2(address to, uint256 am) public { + address from_msgsender = msg.sender; + int_transferFrom(from_msgsender, to, am); // from is constant + } + + // This is not detected + function bad2(address from, address to, uint256 am) public { + address from_msgsender = msg.sender; + int_transferFrom(from_msgsender, to, amount); // from is not a constant + } + + function int_transferFrom(address from, address to, uint256 amount) internal { + erc20.transferFrom(from, to, amount); // not a constant = not a constant U constant + } +} \ No newline at end of file diff --git a/tests/unit/core/test_must_depend_on.py b/tests/unit/core/test_must_depend_on.py new file mode 100644 index 0000000000..c766bd7297 --- /dev/null +++ b/tests/unit/core/test_must_depend_on.py @@ -0,0 +1,30 @@ +from pathlib import Path +from slither import Slither +from slither.analyses.data_dependency.data_dependency import get_must_depends_on +from slither.core.variables.variable import Variable +from slither.core.declarations import SolidityVariable, SolidityVariableComposed +from typing import Union +from slither.slithir.variables import ( + Constant, +) + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" +SUPPORTED_TYPES = Union[Variable, SolidityVariable, Constant] + + +def test_must_depend_on_returns(solc_binary_path): + solc_path = solc_binary_path("0.8.19") + file = Path(TEST_DATA_DIR, "must_depend_on.sol").as_posix() + slither_obj = Slither(file, solc=solc_path) + + for contract in slither_obj.contracts: + for function in contract.functions: + if contract == "Unsafe" and function == "int_transferFrom": + result = get_must_depends_on(function.parameters[0]) + break + assert isinstance(result, list) + assert result[0] == SolidityVariableComposed("msg.sender"), "Output should be msg.sender" + + result = get_must_depends_on(slither_obj.contracts[1].functions[2].parameters[1]) + assert isinstance(result, list) + assert len(result) == 0, "Output should be empty"