Repository: crytic/slither

Issue:
False positive on external-function
When a function is declared by and called within the base class and overridden by the derived class, slither generates an error on the derived class saying that the function could be made external because the derived class doesn't call the function internally. However, the internal linkage is being used by the base class, and Solidity doesn't allow a derived class to change the visibility of a function when overriding it. Feel free to close if you think this detector is working as intended. I've built a minimal example that shows the false positive. 
```
/*
  Test
*/
pragma solidity ^0.5.0;


contract Base {
    // Meant to be customized/overridden by derived classes.
    function abstractFunction() public;
    function callAbstract() external {
        abstractFunction();
    }
}


contract Derived is Base {
    function abstractFunction() public {
        // Some custom functionality.
    }
}
```


Discussion:
Thanks @mrice32 for the report! It's a valid issue.

We are going to update the detector to avoid this FP

Resolving patch:
diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py
--- a/slither/core/declarations/contract.py
+++ b/slither/core/declarations/contract.py
@@ -68,6 +68,14 @@ def inheritance_reverse(self):
     def setInheritance(self, inheritance):
         self._inheritance = inheritance
 
+    @property
+    def derived_contracts(self):
+        '''
+            list(Contract): Return the list of contracts derived from self
+        '''
+        candidates = self.slither.contracts
+        return [c for c in candidates if self in c.inheritance]
+
     @property
     def structures(self):
         '''
@@ -85,12 +93,6 @@ def enums(self):
     def enums_as_dict(self):
         return self._enums
 
-    @property
-    def modifiers(self):
-        '''
-            list(Modifier): List of the modifiers
-        '''
-        return list(self._modifiers.values())
 
     def modifiers_as_dict(self):
         return self._modifiers
@@ -113,6 +115,75 @@ def functions_inherited(self):
         '''
         return [f for f in self.functions if f.contract != self]
 
+    @property
+    def functions_not_inherited(self):
+        '''
+            list(Function): List of the functions defined within the contract (not inherited)
+        '''
+        return [f for f in self.functions if f.contract == self]
+
+    @property
+    def functions_entry_points(self):
+        '''
+            list(Functions): List of public and external functions
+        '''
+        return [f for f in self.functions if f.visibility in ['public', 'external']]
+
+    @property
+    def modifiers(self):
+        '''
+            list(Modifier): List of the modifiers
+        '''
+        return list(self._modifiers.values())
+
+    @property
+    def modifiers_inherited(self):
+        '''
+            list(Modifier): List of the inherited modifiers
+        '''
+        return [m for m in self.modifiers if m.contract != self]
+
+    @property
+    def modifiers_not_inherited(self):
+        '''
+            list(Modifier): List of the modifiers defined within the contract (not inherited)
+        '''
+        return [m for m in self.modifiers if m.contract == self]
+
+    @property
+    def functions_and_modifiers(self):
+        '''
+            list(Function|Modifier): List of the functions and modifiers
+        '''
+        return self.functions + self.modifiers
+
+    @property
+    def functions_and_modifiers_inherited(self):
+        '''
+            list(Function|Modifier): List of the inherited functions and modifiers
+        '''
+        return self.functions_inherited + self.modifiers_inherited
+
+    @property
+    def functions_and_modifiers_not_inherited(self):
+        '''
+            list(Function|Modifier): List of the functions and modifiers defined within the contract (not inherited)
+        '''
+        return self.functions_not_inherited + self.modifiers_not_inherited
+
+    def get_functions_overridden_by(self, function):
+        '''
+            Return the list of functions overriden by the function
+        Args:
+            (core.Function)
+        Returns:
+            list(core.Function)
+
+        '''
+        candidates = [c.functions_not_inherited for c in self.inheritance]
+        candidates = [candidate for sublist in candidates for candidate in sublist]
+        return [f for f in candidates if f.full_name == function.full_name]
+
     @property
     def all_functions_called(self):
         '''
diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py
--- a/slither/core/declarations/function.py
+++ b/slither/core/declarations/function.py
@@ -319,6 +319,18 @@ def full_name(self):
         name, parameters, _ = self.signature
         return name+'('+','.join(parameters)+')'
 
+    @property
+    def functions_shadowed(self):
+        '''
+            Return the list of functions shadowed
+        Returns:
+            list(core.Function)
+
+        '''
+        candidates = [c.functions_not_inherited for c in self.contract.inheritance]
+        candidates = [candidate for sublist in candidates for candidate in sublist]
+        return [f for f in candidates if f.full_name == self.full_name]
+
 
     @property
     def slither(self):
diff --git a/slither/detectors/functions/external_function.py b/slither/detectors/functions/external_function.py
--- a/slither/detectors/functions/external_function.py
+++ b/slither/detectors/functions/external_function.py
@@ -27,7 +27,10 @@ def detect_functions_called(contract):
             (list): List of all InternallCall, SolidityCall
         """
         result = []
+
+        # Obtain all functions reachable by this contract.
         for func in contract.all_functions_called:
+            # Loop through all nodes in the function, add all calls to a list.
             for node in func.nodes:
                 for ir in node.irs:
                     if isinstance(ir, (InternalCall, SolidityCall)):
@@ -36,6 +39,12 @@ def detect_functions_called(contract):
 
     @staticmethod
     def _contains_internal_dynamic_call(contract):
+        """
+        Checks if a contract contains a dynamic call either in a direct definition, or through inheritance.
+
+        Returns:
+            (boolean): True if this contract contains a dynamic call (including through inheritance).
+        """
         for func in contract.all_functions_called:
             for node in func.nodes:
                 for ir in node.irs:
@@ -43,31 +52,126 @@ def _contains_internal_dynamic_call(contract):
                         return True
         return False
 
+    @staticmethod
+    def get_base_most_function(function):
+        """
+        Obtains the base function definition for the provided function. This could be used to obtain the original
+        definition of a function, if the provided function is an override.
+
+        Returns:
+            (function): Returns the base-most function of a provided function. (The original definition).
+        """
+        # Loop through the list of inherited contracts and this contract, to find the first function instance which
+        # matches this function's signature. Note here that `inheritance` is in order from most basic to most extended.
+        for contract in function.contract.inheritance + [function.contract]:
+
+            # Loop through the functions not inherited (explicitly defined in this contract).
+            for f in contract.functions_not_inherited:
+
+                # If it matches names, this is the base most function.
+                if f.full_name == function.full_name:
+                    return f
+
+        # Somehow we couldn't resolve it, which shouldn't happen, as the provided function should be found if we could
+        # not find some any more basic.
+        raise Exception("Could not resolve the base-most function for the provided function.")
+
+    @staticmethod
+    def get_all_function_definitions(base_most_function):
+        """
+        Obtains all function definitions given a base-most function. This includes the provided function, plus any
+        overrides of that function.
+
+        Returns:
+            (list): Returns any the provided function and any overriding functions defined for it.
+        """
+        # We assume the provided function is the base-most function, so we check all derived contracts
+        # for a redefinition
+        return [base_most_function] + [function for derived_contract in base_most_function.contract.derived_contracts
+                                       for function in derived_contract.functions
+                                       if function.full_name == base_most_function.full_name]
+
     def detect(self):
         results = []
 
-        public_function_calls = []
         all_info = ''
 
-        for contract in self.slither.contracts_derived:
+        # Create a set to track contracts with dynamic calls. All contracts with dynamic calls could potentially be
+        # calling functions internally, and thus we can't assume any function in such contracts isn't called by them.
+        dynamic_call_contracts = set()
+
+        # Create a completed functions set to skip over functions already processed (any functions which are the base
+        # of, or override hierarchically are processed together).
+        completed_functions = set()
+
+        # First we build our set of all contracts with dynamic calls
+        for contract in self.contracts:
             if self._contains_internal_dynamic_call(contract):
+                dynamic_call_contracts.add(contract)
+
+        # Loop through all not-inherited contracts.
+        for contract in self.slither.contracts_derived:
+
+            # Filter false-positives: Immediately filter this contract if it's in blacklist
+            if contract in dynamic_call_contracts:
                 continue
 
-            func_list = self.detect_functions_called(contract)
-            public_function_calls.extend(func_list)
-
-            for func in [f for f in contract.functions if f.visibility == 'public' and\
-                                                           not f in public_function_calls and\
-                                                           not f.is_constructor]:
-                txt = "{}.{} ({}) should be declared external\n"
-                info = txt.format(func.contract.name,
-                                  func.name,
-                                  func.source_mapping_str)
-                all_info += info
-
-                json = self.generate_json_result(info)
-                self.add_function_to_json(func, json)
-                results.append(json)
+            # Next we'll want to loop through all functions defined directly in this contract.
+            for function in contract.functions_not_inherited:
+
+                # If the function is a constructor, or is public, we skip it.
+                if function.is_constructor or function.visibility != "public":
+                    continue
+
+                # Optimization: If this function has already been processed, we stop.
+                if function in completed_functions:
+                    continue
+
+                # Get the base-most function to know our origin of this function.
+                base_most_function = self.get_base_most_function(function)
+
+                # Get all possible contracts which can call this function (or an override).
+                all_possible_sources = [base_most_function.contract] + base_most_function.contract.derived_contracts
+
+                # Get all function signatures (overloaded and not), mark as completed and we process them now.
+                # Note: We mark all function definitions as the same, as they must all share visibility to override.
+                all_function_definitions = set(self.get_all_function_definitions(base_most_function))
+                completed_functions = completed_functions.union(all_function_definitions)
+
+                # Filter false-positives: Determine if any of these sources have dynamic calls, if so, flag all of these
+                # function definitions, and then flag all functions in all contracts that make dynamic calls.
+                sources_with_dynamic_calls = set(all_possible_sources) & dynamic_call_contracts
+                if sources_with_dynamic_calls:
+                    functions_in_dynamic_call_sources = set([f for dyn_contract in sources_with_dynamic_calls
+                                                             for f in dyn_contract if not f.is_constructor])
+                    completed_functions = completed_functions.union(functions_in_dynamic_call_sources)
+                    continue
+
+                # Detect all functions called in each source, if any match our current signature, we skip
+                # otherwise, this is a candidate (in all sources) to be changed visibility for.
+                is_called = False
+                for possible_source in all_possible_sources:
+                    functions_called = self.detect_functions_called(possible_source)
+                    if set(functions_called) & all_function_definitions:
+                        is_called = True
+                        break
+
+                # If any of this function's definitions are called, we skip it.
+                if is_called:
+                    continue
+
+                # Loop for each function definition, and recommend it be declared external.
+                for function_definition in all_function_definitions:
+                    txt = "{}.{} ({}) should be declared external\n"
+                    info = txt.format(function_definition.contract.name,
+                                      function_definition.name,
+                                      function_definition.source_mapping_str)
+                    all_info += info
+
+                    json = self.generate_json_result(info)
+                    self.add_function_to_json(function_definition, json)
+                    results.append(json)
+
         if all_info != '':
             self.log(all_info)
         return results


Test patch:
diff --git a/scripts/tests_generate_expected_json_4.sh b/scripts/tests_generate_expected_json_4.sh
--- a/scripts/tests_generate_expected_json_4.sh
+++ b/scripts/tests_generate_expected_json_4.sh
@@ -14,28 +14,29 @@ generate_expected_json(){
 
 }
 
-generate_expected_json tests/uninitialized.sol "uninitialized-state"
-generate_expected_json tests/backdoor.sol "backdoor"
-generate_expected_json tests/backdoor.sol "suicidal"
-generate_expected_json tests/pragma.0.4.24.sol "pragma"
-generate_expected_json tests/old_solc.sol.json "solc-version"
-generate_expected_json tests/reentrancy.sol "reentrancy"
-generate_expected_json tests/uninitialized_storage_pointer.sol "uninitialized-storage"
-generate_expected_json tests/tx_origin.sol "tx-origin"
-generate_expected_json tests/unused_state.sol "unused-state"
-generate_expected_json tests/locked_ether.sol "locked-ether"
-generate_expected_json tests/arbitrary_send.sol "arbitrary-send"
-generate_expected_json tests/inline_assembly_contract.sol "assembly"
-generate_expected_json tests/inline_assembly_library.sol "assembly"
-generate_expected_json tests/low_level_calls.sol "low-level-calls"
-generate_expected_json tests/const_state_variables.sol "constable-states"
-generate_expected_json tests/external_function.sol "external-function"
-generate_expected_json tests/naming_convention.sol "naming-convention"
-generate_expected_json tests/uninitialized_local_variable.sol "uninitialized-local"
-generate_expected_json tests/controlled_delegatecall.sol "controlled-delegatecall"
-generate_expected_json tests/constant.sol "constant-function"
-generate_expected_json tests/unused_return.sol "unused-return"
-generate_expected_json tests/shadowing_state_variable.sol "shadowing-state"
-generate_expected_json tests/shadowing_abstract.sol "shadowing-abstract"
-generate_expected_json tests/timestamp.sol "timestamp"
-generate_expected_json tests/multiple_calls_in_loop.sol "calls-loop"
+#generate_expected_json tests/uninitialized.sol "uninitialized-state"
+#generate_expected_json tests/backdoor.sol "backdoor"
+#generate_expected_json tests/backdoor.sol "suicidal"
+#generate_expected_json tests/pragma.0.4.24.sol "pragma"
+#generate_expected_json tests/old_solc.sol.json "solc-version"
+#generate_expected_json tests/reentrancy.sol "reentrancy"
+#generate_expected_json tests/uninitialized_storage_pointer.sol "uninitialized-storage"
+#generate_expected_json tests/tx_origin.sol "tx-origin"
+#generate_expected_json tests/unused_state.sol "unused-state"
+#generate_expected_json tests/locked_ether.sol "locked-ether"
+#generate_expected_json tests/arbitrary_send.sol "arbitrary-send"
+#generate_expected_json tests/inline_assembly_contract.sol "assembly"
+#generate_expected_json tests/inline_assembly_library.sol "assembly"
+#generate_expected_json tests/low_level_calls.sol "low-level-calls"
+#generate_expected_json tests/const_state_variables.sol "constable-states"
+#generate_expected_json tests/external_function.sol "external-function"
+generate_expected_json tests/external_function_2.sol "external-function"
+#generate_expected_json tests/naming_convention.sol "naming-convention"
+#generate_expected_json tests/uninitialized_local_variable.sol "uninitialized-local"
+#generate_expected_json tests/controlled_delegatecall.sol "controlled-delegatecall"
+#generate_expected_json tests/constant.sol "constant-function"
+#generate_expected_json tests/unused_return.sol "unused-return"
+#generate_expected_json tests/shadowing_state_variable.sol "shadowing-state"
+#generate_expected_json tests/shadowing_abstract.sol "shadowing-abstract"
+#generate_expected_json tests/timestamp.sol "timestamp"
+#generate_expected_json tests/multiple_calls_in_loop.sol "calls-loop"
diff --git a/scripts/tests_generate_expected_json_5.sh b/scripts/tests_generate_expected_json_5.sh
--- a/scripts/tests_generate_expected_json_5.sh
+++ b/scripts/tests_generate_expected_json_5.sh
@@ -14,16 +14,16 @@ generate_expected_json(){
 
 }
 
-generate_expected_json tests/uninitialized-0.5.1.sol "uninitialized-state"
+#generate_expected_json tests/uninitialized-0.5.1.sol "uninitialized-state"
 #generate_expected_json tests/backdoor.sol "backdoor"
 #generate_expected_json tests/backdoor.sol "suicidal"
 #generate_expected_json tests/pragma.0.4.24.sol "pragma"
 #generate_expected_json tests/old_solc.sol.json "solc-version"
-generate_expected_json tests/reentrancy-0.5.1.sol "reentrancy"
+#generate_expected_json tests/reentrancy-0.5.1.sol "reentrancy"
 #generate_expected_json tests/uninitialized_storage_pointer.sol "uninitialized-storage"
-generate_expected_json tests/tx_origin-0.5.1.sol "tx-origin"
-generate_expected_json tests/locked_ether-0.5.1.sol "locked-ether"
-generate_expected_json tests/arbitrary_send-0.5.1.sol "arbitrary-send"
-generate_expected_json tests/inline_assembly_contract-0.5.1.sol "assembly"
-generate_expected_json tests/inline_assembly_library-0.5.1.sol "assembly"
-generate_expected_json tests/constant-0.5.1.sol "constant-function"
+#generate_expected_json tests/tx_origin-0.5.1.sol "tx-origin"
+#generate_expected_json tests/locked_ether-0.5.1.sol "locked-ether"
+#generate_expected_json tests/arbitrary_send-0.5.1.sol "arbitrary-send"
+#generate_expected_json tests/inline_assembly_contract-0.5.1.sol "assembly"
+#generate_expected_json tests/inline_assembly_library-0.5.1.sol "assembly"
+#generate_expected_json tests/constant-0.5.1.sol "constant-function"
diff --git a/scripts/travis_test_4.sh b/scripts/travis_test_4.sh
--- a/scripts/travis_test_4.sh
+++ b/scripts/travis_test_4.sh
@@ -81,6 +81,7 @@ test_slither tests/inline_assembly_library.sol "assembly"
 test_slither tests/low_level_calls.sol "low-level-calls"
 test_slither tests/const_state_variables.sol "constable-states"
 test_slither tests/external_function.sol "external-function"
+test_slither tests/external_function_2.sol "external-function"
 test_slither tests/naming_convention.sol "naming-convention"
 #test_slither tests/complex_func.sol "complex-function"
 test_slither tests/controlled_delegatecall.sol "controlled-delegatecall"
diff --git a/scripts/travis_test_5.sh b/scripts/travis_test_5.sh
--- a/scripts/travis_test_5.sh
+++ b/scripts/travis_test_5.sh
@@ -79,8 +79,9 @@ test_slither tests/inline_assembly_library-0.5.1.sol "assembly"
 test_slither tests/low_level_calls.sol "low-level-calls"
 test_slither tests/const_state_variables.sol "constable-states"
 test_slither tests/external_function.sol "external-function"
+test_slither tests/external_function_2.sol "external-function"
 test_slither tests/naming_convention.sol "naming-convention"
-#test_slither tests/complex_func.sol "complex-function"
+##test_slither tests/complex_func.sol "complex-function"
 test_slither tests/controlled_delegatecall.sol "controlled-delegatecall"
 test_slither tests/constant-0.5.1.sol "constant-function"
 test_slither tests/unused_return.sol "unused-return"
diff --git a/tests/expected_json/external_function_2.external-function.json b/tests/expected_json/external_function_2.external-function.json
new file mode 100644
--- /dev/null
+++ b/tests/expected_json/external_function_2.external-function.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/tests/external_function.sol b/tests/external_function.sol
--- a/tests/external_function.sol
+++ b/tests/external_function.sol
@@ -1,6 +1,6 @@
 //pragma solidity ^0.4.24;
 
-import "./external_function_test_2.sol";
+import "./external_function_import.sol";
 
 contract ContractWithFunctionCalledSuper is ContractWithFunctionCalled {
     function callWithSuper() public {
diff --git a/tests/external_function_2.sol b/tests/external_function_2.sol
new file mode 100644
--- /dev/null
+++ b/tests/external_function_2.sol
@@ -0,0 +1,55 @@
+// This tests against false-positives. This test should output no recommendations from the external-function detector.
+
+
+contract ContractWithBaseFunctionCalled {
+    function getsCalledByBase() public;
+    function callsOverrideMe() external {
+        getsCalledByBase();
+    }
+}
+
+
+contract DerivingContractWithBaseCalled is ContractWithBaseFunctionCalled {
+    function getsCalledByBase() public {
+        // This should not be recommended to be marked external because it is called by the base class.
+    }
+}
+
+
+// All the contracts below should not recommend changing to external since inherited contracts have dynamic calls.
+contract ContractWithDynamicCall {
+    function() returns(uint) ptr;
+
+    function test1() public returns(uint){
+        return 1;
+    }
+
+    function test2() public returns(uint){
+        return 2;
+    }
+
+    function setTest1() external{
+        ptr = test1;
+    }
+
+    function setTest2() external{
+        ptr = test2;
+    }
+
+    function exec() external returns(uint){
+        return ptr();
+    }
+}
+
+contract DerivesFromDynamicCall is ContractWithDynamicCall{
+    function getsCalledDynamically() public returns (uint){
+        // This should not be recommended because it is called dynamically.
+        return 3;
+    }
+    function setTest3() public {
+        // This should not be recommended because we inherit from a contract that calls dynamically, and we cannot be
+        // sure it did not somehow call this function.
+
+        ptr = getsCalledDynamically;
+    }
+}
diff --git a/tests/external_function_test_2.sol b/tests/external_function_import.sol
similarity index 68%
rename from tests/external_function_test_2.sol
rename to tests/external_function_import.sol
--- a/tests/external_function_test_2.sol
+++ b/tests/external_function_import.sol
@@ -1,4 +1,4 @@
-//pragma solidity ^0.4.24;
+// This file is imported by external_function.sol
 
 contract ContractWithFunctionCalled {
     function funcCalled() external  {