Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: Prune all unnecessary functions #1

Open
plotchy opened this issue May 14, 2024 · 2 comments
Open

Idea: Prune all unnecessary functions #1

plotchy opened this issue May 14, 2024 · 2 comments

Comments

@plotchy
Copy link

plotchy commented May 14, 2024

Many functions in a solidity project can be deployed but have no actual use onchain.

For instance, a library file may have 100 functions, but only 1 is actually used by the contracts.

What if this tool could prune all functions that have no actual way to run the logic onchain? Removing those would save effort when reviewing.

Here is one way to do it

@property
def auditable_functions(self) -> List["Function"]:
    """
    list(Function): List of functions+modifiers+libraries+constructors used by the contract. Similar to all_functions_called, 
        but adds in used library fns and doesn't include unused fns/dead code.
    Essentially, this is the full list of functions that are useable by external actors or the contract itself, and should be audited.
    """
    if self._auditable_functions is None:
        # find all entry points + internal/private calls and modifiers used by them
        
        all_entry_functions = [f for f in self.functions_entry_points if f.is_implemented]
        all_entries_and_internal_calls = [ic for f in all_entry_functions for ic in f.all_internal_calls() if not isinstance(ic, SolidityFunction) and ic.is_implemented] + all_entry_functions # don't include SolidityFunction's in this
        all_entries_and_internal_calls = list(set(all_entries_and_internal_calls))

        add_modifiers = [m for fn in all_entries_and_internal_calls for m in fn.modifiers if not isinstance(m, Contract) and not m.is_shadowed] + all_entries_and_internal_calls # save constructors for later
        conclusive_contract_calls = list(set(add_modifiers)) # conclusive set of all entries/int&priv calls and modifiers used

        # find library functions used by these entries/int&private-calls/modifiers, and the int&private-calls/modifiers they use
        library_calls = [f.all_library_calls() for f in conclusive_contract_calls]
        library_calls = [item for sublist in library_calls for item in sublist] # blame montyly for making this a list of tuples lol
        # List[Tuple[Contract, Function]] == List[LibraryCall]
        library_calls = [lib[1] for lib in library_calls if isinstance(lib, tuple)] # unwrapping the Function from the library tuple
        library_calls = list(set(library_calls))

        add_library_internal_calls = [ic for f in library_calls for ic in f.all_internal_calls() if not isinstance(ic, SolidityFunction) and ic.is_implemented] + library_calls # don't include SolidityFunction's in this
        add_library_internal_calls = list(set(add_library_internal_calls))

        add_library_modifiers = [m for fn in add_library_internal_calls for m in fn.modifiers if not isinstance(m, Contract) and not m.is_shadowed] + add_library_internal_calls
        conclusive_library_calls = list(set(add_library_modifiers)) # conclusive set of all library entries/int&priv calls and modifiers used

        # add list of all conclusive calls + turn into set to remove duplicates
        set_all_calls = set(conclusive_contract_calls + conclusive_library_calls + self.constructors)

        self._auditable_functions = [c for c in set_all_calls if isinstance(c, Function) and not isinstance(c.entry_point, type(None)) and c.is_implemented]
        
    return self._auditable_functions
@property
def auditable_contracts(self) -> List[Contract]:
    """
    list(Contract): List of contracts that are leafs and not interfaces, abstract or libraries.
        What remains are deployable, fully-derived, fully-implemented, and thus auditable contracts.
    """
    inheritances = [x.inheritance for x in self.contracts]
    inheritance = [item for sublist in inheritances for item in sublist]
    return [c for c in self.contracts if c not in inheritance and not c.is_interface and not c.is_abstract and not c.is_library]
@plotchy
Copy link
Author

plotchy commented May 14, 2024

Even better if you could export the files after this pruning and still have them be compilable

@shortdoom
Copy link
Owner

shortdoom commented May 15, 2024

Yes, it's a good idea to add exporting capability like such. I also wanted to generate some basic reports for each contract scanned, like: "What are the main entry points?" and "What other functions and variables do those entry points taint?". Including cleaner source code without the dead-code functions could go into it.

As for excluding unused library functions, it's already partially achieved under the hood!

library_functions.webm

First, if detectors successfully ran, there will be a "dead-code" detector/check available from Slither. It's an informational level detector. Notice that "jump-to-line" will work fine on such findings; it will focus on the library function in the source code view. However, the same functions won't be displayed in the list of functions (search results). That's because Betterscan only includes target contract functions that are declared in the target contract or its immediate inheritance.

if contract.name == self.name:
self.root_contract = contract
self.root_contract_path = path_to_source
self.property_check.set_root_contract(contract)
# contract Test is A,B,C - sometimes A,B,C may be main entry points
contracts_inherited = [
parent
for parent in contract.immediate_inheritance
if not parent.is_interface
]
for inherited in contracts_inherited:
for function in inherited.functions:
if function.contract_declarer.is_interface:
continue
if (
function.full_name
!= "slitherConstructorConstantVariables()"
and function.full_name != "slitherConstructorVariables()"
):
self.analyze_function(function, True)
# main contract functions
functions_declared = [
function for function in contract.functions_declared
]
for function in contract.functions:
if (
function.full_name != "slitherConstructorConstantVariables()"
and function.full_name != "slitherConstructorVariables()"
):
if function in functions_declared:
self.analyze_function(function)

So, if needed, we have the full original source code with unused functions, but those never get included in the search results/function list display (as they have no effect on anything). Those also won't be included in sessionData.json in the files/out directory (from which the UI reads).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants