mirror of https://github.com/crytic/slither
pull/1535/head
commit
19b3af3449
@ -0,0 +1,87 @@ |
||||
--- |
||||
name: CI |
||||
|
||||
defaults: |
||||
run: |
||||
shell: bash |
||||
|
||||
on: |
||||
workflow_dispatch: |
||||
pull_request: |
||||
paths: |
||||
- 'slither/tools/doctor/**' |
||||
- '.github/workflows/doctor.yml' |
||||
|
||||
jobs: |
||||
slither-doctor: |
||||
runs-on: ${{ matrix.os }} |
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
os: ["ubuntu-latest", "windows-2022"] |
||||
python: ["3.8", "3.9", "3.10", "3.11"] |
||||
exclude: |
||||
# strange failure |
||||
- os: windows-2022 |
||||
python: 3.8 |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
|
||||
- name: Set up Python ${{ matrix.python }} |
||||
uses: actions/setup-python@v4 |
||||
with: |
||||
python-version: ${{ matrix.python }} |
||||
|
||||
- name: Try system-wide Slither |
||||
run: | |
||||
echo "::group::Install slither" |
||||
pip3 install . |
||||
echo "::endgroup::" |
||||
|
||||
# escape cwd so python doesn't pick up local module |
||||
cd / |
||||
|
||||
echo "::group::Via module" |
||||
python3 -m slither.tools.doctor . |
||||
echo "::endgroup::" |
||||
|
||||
echo "::group::Via binary" |
||||
slither-doctor . |
||||
echo "::endgroup::" |
||||
|
||||
- name: Try user Slither |
||||
run: | |
||||
echo "::group::Install slither" |
||||
pip3 install --user . |
||||
echo "::endgroup::" |
||||
|
||||
# escape cwd so python doesn't pick up local module |
||||
cd / |
||||
|
||||
echo "::group::Via module" |
||||
python3 -m slither.tools.doctor . |
||||
echo "::endgroup::" |
||||
|
||||
echo "::group::Via binary" |
||||
slither-doctor . |
||||
echo "::endgroup::" |
||||
|
||||
- name: Try venv Slither |
||||
run: | |
||||
echo "::group::Install slither" |
||||
python3 -m venv venv |
||||
source venv/bin/activate || source venv/Scripts/activate |
||||
hash -r |
||||
pip3 install . |
||||
echo "::endgroup::" |
||||
|
||||
# escape cwd so python doesn't pick up local module |
||||
cd / |
||||
|
||||
echo "::group::Via module" |
||||
python3 -m slither.tools.doctor . |
||||
echo "::endgroup::" |
||||
|
||||
echo "::group::Via binary" |
||||
slither-doctor . |
||||
echo "::endgroup::" |
@ -0,0 +1,6 @@ |
||||
* @montyly @0xalpharush @smonicas |
||||
/slither/tools/read_storage/ @0xalpharush |
||||
/slither/tools/doctor/ @elopez |
||||
/slither/slithir/ @montyly |
||||
/slither/analyses/ @montyly |
||||
/.github/workflows/ @elopez |
@ -0,0 +1,18 @@ |
||||
from typing import TYPE_CHECKING, List, Dict, Union |
||||
|
||||
from slither.core.solidity_types.type import Type |
||||
from slither.core.declarations.top_level import TopLevel |
||||
|
||||
if TYPE_CHECKING: |
||||
from slither.core.scope.scope import FileScope |
||||
|
||||
|
||||
class UsingForTopLevel(TopLevel): |
||||
def __init__(self, scope: "FileScope"): |
||||
super().__init__() |
||||
self._using_for: Dict[Union[str, Type], List[Type]] = {} |
||||
self.file_scope: "FileScope" = scope |
||||
|
||||
@property |
||||
def using_for(self) -> Dict[Type, List[Type]]: |
||||
return self._using_for |
@ -0,0 +1,45 @@ |
||||
from typing import List, Dict |
||||
from slither.utils.output import Output |
||||
from slither.core.compilation_unit import SlitherCompilationUnit |
||||
from slither.formatters.variables.unchanged_state_variables import custom_format |
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from .unchanged_state_variables import UnchangedStateVariables |
||||
|
||||
|
||||
class CouldBeConstant(AbstractDetector): |
||||
""" |
||||
State variables that could be declared as constant. |
||||
Not all types for constants are implemented in Solidity as of 0.4.25. |
||||
The only supported types are value types and strings (ElementaryType). |
||||
Reference: https://solidity.readthedocs.io/en/latest/contracts.html#constant-state-variables |
||||
""" |
||||
|
||||
ARGUMENT = "constable-states" |
||||
HELP = "State variables that could be declared constant" |
||||
IMPACT = DetectorClassification.OPTIMIZATION |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant" |
||||
|
||||
WIKI_TITLE = "State variables that could be declared constant" |
||||
WIKI_DESCRIPTION = "State variables that are not updated following deployment should be declared constant to save gas." |
||||
WIKI_RECOMMENDATION = "Add the `constant` attribute to state variables that never change." |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
"""Detect state variables that could be constant""" |
||||
results = {} |
||||
|
||||
unchanged_state_variables = UnchangedStateVariables(self.compilation_unit) |
||||
unchanged_state_variables.detect() |
||||
|
||||
for variable in unchanged_state_variables.constant_candidates: |
||||
results[variable.canonical_name] = self.generate_result( |
||||
[variable, " should be constant \n"] |
||||
) |
||||
|
||||
# Order by canonical name for deterministic results |
||||
return [results[k] for k in sorted(results)] |
||||
|
||||
@staticmethod |
||||
def _format(compilation_unit: SlitherCompilationUnit, result: Dict) -> None: |
||||
custom_format(compilation_unit, result, "constant") |
@ -0,0 +1,44 @@ |
||||
from typing import List, Dict |
||||
from slither.utils.output import Output |
||||
from slither.core.compilation_unit import SlitherCompilationUnit |
||||
from slither.formatters.variables.unchanged_state_variables import custom_format |
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from .unchanged_state_variables import UnchangedStateVariables |
||||
|
||||
|
||||
class CouldBeImmutable(AbstractDetector): |
||||
""" |
||||
State variables that could be declared immutable. |
||||
# Immutable attribute available in Solidity 0.6.5 and above |
||||
# https://blog.soliditylang.org/2020/04/06/solidity-0.6.5-release-announcement/ |
||||
""" |
||||
|
||||
# VULNERABLE_SOLC_VERSIONS = |
||||
ARGUMENT = "immutable-states" |
||||
HELP = "State variables that could be declared immutable" |
||||
IMPACT = DetectorClassification.OPTIMIZATION |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-immutable" |
||||
|
||||
WIKI_TITLE = "State variables that could be declared immutable" |
||||
WIKI_DESCRIPTION = "State variables that are not updated following deployment should be declared immutable to save gas." |
||||
WIKI_RECOMMENDATION = "Add the `immutable` attribute to state variables that never change or are set only in the constructor." |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
"""Detect state variables that could be immutable""" |
||||
results = {} |
||||
unchanged_state_variables = UnchangedStateVariables(self.compilation_unit) |
||||
unchanged_state_variables.detect() |
||||
|
||||
for variable in unchanged_state_variables.immutable_candidates: |
||||
results[variable.canonical_name] = self.generate_result( |
||||
[variable, " should be immutable \n"] |
||||
) |
||||
|
||||
# Order by canonical name for deterministic results |
||||
return [results[k] for k in sorted(results)] |
||||
|
||||
@staticmethod |
||||
def _format(compilation_unit: SlitherCompilationUnit, result: Dict) -> None: |
||||
custom_format(compilation_unit, result, "immutable") |
@ -1,125 +0,0 @@ |
||||
""" |
||||
Module detecting state variables that could be declared as constant |
||||
""" |
||||
from typing import Set, List, Dict |
||||
|
||||
from slither.core.compilation_unit import SlitherCompilationUnit |
||||
from slither.core.solidity_types.elementary_type import ElementaryType |
||||
from slither.core.solidity_types.user_defined_type import UserDefinedType |
||||
from slither.core.variables.variable import Variable |
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.utils.output import Output |
||||
from slither.visitors.expression.export_values import ExportValues |
||||
from slither.core.declarations import Contract, Function |
||||
from slither.core.declarations.solidity_variables import SolidityFunction |
||||
from slither.core.variables.state_variable import StateVariable |
||||
from slither.formatters.variables.possible_const_state_variables import custom_format |
||||
|
||||
|
||||
def _is_valid_type(v: StateVariable) -> bool: |
||||
t = v.type |
||||
if isinstance(t, ElementaryType): |
||||
return True |
||||
if isinstance(t, UserDefinedType) and isinstance(t.type, Contract): |
||||
return True |
||||
return False |
||||
|
||||
|
||||
def _valid_candidate(v: StateVariable) -> bool: |
||||
return _is_valid_type(v) and not (v.is_constant or v.is_immutable) |
||||
|
||||
|
||||
def _is_constant_var(v: Variable) -> bool: |
||||
if isinstance(v, StateVariable): |
||||
return v.is_constant |
||||
return False |
||||
|
||||
|
||||
class ConstCandidateStateVars(AbstractDetector): |
||||
""" |
||||
State variables that could be declared as constant detector. |
||||
Not all types for constants are implemented in Solidity as of 0.4.25. |
||||
The only supported types are value types and strings (ElementaryType). |
||||
Reference: https://solidity.readthedocs.io/en/latest/contracts.html#constant-state-variables |
||||
""" |
||||
|
||||
ARGUMENT = "constable-states" |
||||
HELP = "State variables that could be declared constant" |
||||
IMPACT = DetectorClassification.OPTIMIZATION |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant" |
||||
|
||||
WIKI_TITLE = "State variables that could be declared constant" |
||||
WIKI_DESCRIPTION = "Constant state variables should be declared constant to save gas." |
||||
WIKI_RECOMMENDATION = "Add the `constant` attributes to state variables that never change." |
||||
|
||||
# https://solidity.readthedocs.io/en/v0.5.2/contracts.html#constant-state-variables |
||||
valid_solidity_function = [ |
||||
SolidityFunction("keccak256()"), |
||||
SolidityFunction("keccak256(bytes)"), |
||||
SolidityFunction("sha256()"), |
||||
SolidityFunction("sha256(bytes)"), |
||||
SolidityFunction("ripemd160()"), |
||||
SolidityFunction("ripemd160(bytes)"), |
||||
SolidityFunction("ecrecover(bytes32,uint8,bytes32,bytes32)"), |
||||
SolidityFunction("addmod(uint256,uint256,uint256)"), |
||||
SolidityFunction("mulmod(uint256,uint256,uint256)"), |
||||
] |
||||
|
||||
def _constant_initial_expression(self, v: Variable) -> bool: |
||||
if not v.expression: |
||||
return True |
||||
|
||||
export = ExportValues(v.expression) |
||||
values = export.result() |
||||
if not values: |
||||
return True |
||||
if all((val in self.valid_solidity_function or _is_constant_var(val) for val in values)): |
||||
return True |
||||
return False |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
"""Detect state variables that could be const""" |
||||
results = [] |
||||
|
||||
all_variables_l = [c.state_variables for c in self.compilation_unit.contracts] |
||||
all_variables: Set[StateVariable] = { |
||||
item for sublist in all_variables_l for item in sublist |
||||
} |
||||
all_non_constant_elementary_variables = {v for v in all_variables if _valid_candidate(v)} |
||||
|
||||
all_functions_nested = [c.all_functions_called for c in self.compilation_unit.contracts] |
||||
all_functions = list( |
||||
{ |
||||
item1 |
||||
for sublist in all_functions_nested |
||||
for item1 in sublist |
||||
if isinstance(item1, Function) |
||||
} |
||||
) |
||||
|
||||
all_variables_written = [ |
||||
f.state_variables_written for f in all_functions if not f.is_constructor_variables |
||||
] |
||||
all_variables_written = {item for sublist in all_variables_written for item in sublist} |
||||
|
||||
constable_variables: List[Variable] = [ |
||||
v |
||||
for v in all_non_constant_elementary_variables |
||||
if (v not in all_variables_written) and self._constant_initial_expression(v) |
||||
] |
||||
# Order for deterministic results |
||||
constable_variables = sorted(constable_variables, key=lambda x: x.canonical_name) |
||||
|
||||
# Create a result for each finding |
||||
for v in constable_variables: |
||||
info = [v, " should be constant\n"] |
||||
json = self.generate_result(info) |
||||
results.append(json) |
||||
|
||||
return results |
||||
|
||||
@staticmethod |
||||
def _format(compilation_unit: SlitherCompilationUnit, result: Dict) -> None: |
||||
custom_format(compilation_unit, result) |
@ -0,0 +1,125 @@ |
||||
""" |
||||
Module detecting state variables that could be declared as constant |
||||
""" |
||||
from typing import Set, List |
||||
from packaging import version |
||||
from slither.core.compilation_unit import SlitherCompilationUnit |
||||
from slither.core.solidity_types.elementary_type import ElementaryType |
||||
from slither.core.solidity_types.user_defined_type import UserDefinedType |
||||
from slither.core.variables.variable import Variable |
||||
|
||||
from slither.visitors.expression.export_values import ExportValues |
||||
from slither.core.declarations import Contract, Function |
||||
from slither.core.declarations.solidity_variables import SolidityFunction |
||||
from slither.core.variables.state_variable import StateVariable |
||||
from slither.core.expressions import CallExpression, NewContract |
||||
|
||||
|
||||
def _is_valid_type(v: StateVariable) -> bool: |
||||
t = v.type |
||||
if isinstance(t, ElementaryType): |
||||
return True |
||||
if isinstance(t, UserDefinedType) and isinstance(t.type, Contract): |
||||
return True |
||||
return False |
||||
|
||||
|
||||
def _valid_candidate(v: StateVariable) -> bool: |
||||
return _is_valid_type(v) and not (v.is_constant or v.is_immutable) |
||||
|
||||
|
||||
def _is_constant_var(v: Variable) -> bool: |
||||
if isinstance(v, StateVariable): |
||||
return v.is_constant |
||||
return False |
||||
|
||||
|
||||
# https://solidity.readthedocs.io/en/v0.5.2/contracts.html#constant-state-variables |
||||
valid_solidity_function = [ |
||||
SolidityFunction("keccak256()"), |
||||
SolidityFunction("keccak256(bytes)"), |
||||
SolidityFunction("sha256()"), |
||||
SolidityFunction("sha256(bytes)"), |
||||
SolidityFunction("ripemd160()"), |
||||
SolidityFunction("ripemd160(bytes)"), |
||||
SolidityFunction("ecrecover(bytes32,uint8,bytes32,bytes32)"), |
||||
SolidityFunction("addmod(uint256,uint256,uint256)"), |
||||
SolidityFunction("mulmod(uint256,uint256,uint256)"), |
||||
] |
||||
|
||||
|
||||
def _constant_initial_expression(v: Variable) -> bool: |
||||
if not v.expression: |
||||
return True |
||||
|
||||
# B b = new B(); b cannot be constant, so filter out and recommend it be immutable |
||||
if isinstance(v.expression, CallExpression) and isinstance(v.expression.called, NewContract): |
||||
return False |
||||
|
||||
export = ExportValues(v.expression) |
||||
values = export.result() |
||||
if not values: |
||||
return True |
||||
|
||||
return all((val in valid_solidity_function or _is_constant_var(val) for val in values)) |
||||
|
||||
|
||||
class UnchangedStateVariables: |
||||
""" |
||||
Find state variables that could be declared as constant or immutable (not written after deployment). |
||||
""" |
||||
|
||||
def __init__(self, compilation_unit: SlitherCompilationUnit): |
||||
self.compilation_unit = compilation_unit |
||||
self._constant_candidates: List[StateVariable] = [] |
||||
self._immutable_candidates: List[StateVariable] = [] |
||||
|
||||
@property |
||||
def immutable_candidates(self) -> List[StateVariable]: |
||||
"""Return the immutable candidates""" |
||||
return self._immutable_candidates |
||||
|
||||
@property |
||||
def constant_candidates(self) -> List[StateVariable]: |
||||
"""Return the constant candidates""" |
||||
return self._constant_candidates |
||||
|
||||
def detect(self): |
||||
"""Detect state variables that could be constant or immutable""" |
||||
for c in self.compilation_unit.contracts_derived: |
||||
variables = [] |
||||
functions = [] |
||||
|
||||
variables.append(c.state_variables) |
||||
functions.append(c.all_functions_called) |
||||
|
||||
valid_candidates: Set[StateVariable] = { |
||||
item for sublist in variables for item in sublist if _valid_candidate(item) |
||||
} |
||||
|
||||
all_functions: List[Function] = list( |
||||
{item1 for sublist in functions for item1 in sublist if isinstance(item1, Function)} |
||||
) |
||||
|
||||
variables_written = [] |
||||
constructor_variables_written = [] |
||||
variables_initialized = [] |
||||
for f in all_functions: |
||||
if f.is_constructor_variables: |
||||
variables_initialized.extend(f.state_variables_written) |
||||
elif f.is_constructor: |
||||
constructor_variables_written.extend(f.state_variables_written) |
||||
else: |
||||
variables_written.extend(f.state_variables_written) |
||||
|
||||
for v in valid_candidates: |
||||
if v not in variables_written: |
||||
if _constant_initial_expression(v) and v not in constructor_variables_written: |
||||
self.constant_candidates.append(v) |
||||
|
||||
elif ( |
||||
v in constructor_variables_written or v in variables_initialized |
||||
) and version.parse(self.compilation_unit.solc_version) >= version.parse( |
||||
"0.6.5" |
||||
): |
||||
self.immutable_candidates.append(v) |
@ -0,0 +1,167 @@ |
||||
""" |
||||
Using For Top Level module |
||||
""" |
||||
import logging |
||||
from typing import TYPE_CHECKING, Dict, Union |
||||
|
||||
from slither.core.compilation_unit import SlitherCompilationUnit |
||||
from slither.core.declarations import ( |
||||
StructureTopLevel, |
||||
EnumTopLevel, |
||||
) |
||||
from slither.core.declarations.using_for_top_level import UsingForTopLevel |
||||
from slither.core.scope.scope import FileScope |
||||
from slither.core.solidity_types import TypeAliasTopLevel |
||||
from slither.core.solidity_types.user_defined_type import UserDefinedType |
||||
from slither.solc_parsing.declarations.caller_context import CallerContextExpression |
||||
from slither.solc_parsing.solidity_types.type_parsing import parse_type |
||||
|
||||
if TYPE_CHECKING: |
||||
from slither.solc_parsing.slither_compilation_unit_solc import SlitherCompilationUnitSolc |
||||
|
||||
LOGGER = logging.getLogger("UsingForTopLevelSolc") |
||||
|
||||
|
||||
class UsingForTopLevelSolc(CallerContextExpression): # pylint: disable=too-few-public-methods |
||||
""" |
||||
UsingFor class |
||||
""" |
||||
|
||||
def __init__( |
||||
self, |
||||
uftl: UsingForTopLevel, |
||||
top_level_data: Dict, |
||||
slither_parser: "SlitherCompilationUnitSolc", |
||||
) -> None: |
||||
self._type_name = top_level_data["typeName"] |
||||
self._global = top_level_data["global"] |
||||
|
||||
if "libraryName" in top_level_data: |
||||
self._library_name = top_level_data["libraryName"] |
||||
else: |
||||
self._functions = top_level_data["functionList"] |
||||
self._library_name = None |
||||
|
||||
self._using_for = uftl |
||||
self._slither_parser = slither_parser |
||||
|
||||
def analyze(self) -> None: |
||||
type_name = parse_type(self._type_name, self) |
||||
self._using_for.using_for[type_name] = [] |
||||
|
||||
if self._library_name is not None: |
||||
library_name = parse_type(self._library_name, self) |
||||
self._using_for.using_for[type_name].append(library_name) |
||||
self._propagate_global(type_name) |
||||
else: |
||||
for f in self._functions: |
||||
full_name_split = f["function"]["name"].split(".") |
||||
if len(full_name_split) == 1: |
||||
# Top level function |
||||
function_name: str = full_name_split[0] |
||||
self._analyze_top_level_function(function_name, type_name) |
||||
elif len(full_name_split) == 2: |
||||
# It can be a top level function behind an aliased import |
||||
# or a library function |
||||
first_part = full_name_split[0] |
||||
function_name = full_name_split[1] |
||||
self._check_aliased_import(first_part, function_name, type_name) |
||||
else: |
||||
# MyImport.MyLib.a we don't care of the alias |
||||
library_name_str = full_name_split[1] |
||||
function_name = full_name_split[2] |
||||
self._analyze_library_function(library_name_str, function_name, type_name) |
||||
|
||||
def _check_aliased_import( |
||||
self, |
||||
first_part: str, |
||||
function_name: str, |
||||
type_name: Union[TypeAliasTopLevel, UserDefinedType], |
||||
): |
||||
# We check if the first part appear as alias for an import |
||||
# if it is then function_name must be a top level function |
||||
# otherwise it's a library function |
||||
for i in self._using_for.file_scope.imports: |
||||
if i.alias == first_part: |
||||
self._analyze_top_level_function(function_name, type_name) |
||||
return |
||||
self._analyze_library_function(first_part, function_name, type_name) |
||||
|
||||
def _analyze_top_level_function( |
||||
self, function_name: str, type_name: Union[TypeAliasTopLevel, UserDefinedType] |
||||
) -> None: |
||||
for tl_function in self.compilation_unit.functions_top_level: |
||||
if tl_function.name == function_name: |
||||
self._using_for.using_for[type_name].append(tl_function) |
||||
self._propagate_global(type_name) |
||||
break |
||||
|
||||
def _analyze_library_function( |
||||
self, |
||||
library_name: str, |
||||
function_name: str, |
||||
type_name: Union[TypeAliasTopLevel, UserDefinedType], |
||||
) -> None: |
||||
found = False |
||||
for c in self.compilation_unit.contracts: |
||||
if found: |
||||
break |
||||
if c.name == library_name: |
||||
for cf in c.functions: |
||||
if cf.name == function_name: |
||||
self._using_for.using_for[type_name].append(cf) |
||||
self._propagate_global(type_name) |
||||
found = True |
||||
break |
||||
if not found: |
||||
LOGGER.warning( |
||||
f"Top level using for: Library {library_name} - function {function_name} not found" |
||||
) |
||||
|
||||
def _propagate_global(self, type_name: Union[TypeAliasTopLevel, UserDefinedType]) -> None: |
||||
if self._global: |
||||
for scope in self.compilation_unit.scopes.values(): |
||||
if isinstance(type_name, TypeAliasTopLevel): |
||||
for alias in scope.user_defined_types.values(): |
||||
if alias == type_name: |
||||
scope.using_for_directives.add(self._using_for) |
||||
elif isinstance(type_name, UserDefinedType): |
||||
self._propagate_global_UserDefinedType(scope, type_name) |
||||
else: |
||||
LOGGER.error( |
||||
f"Error when propagating global using for {type_name} {type(type_name)}" |
||||
) |
||||
|
||||
def _propagate_global_UserDefinedType(self, scope: FileScope, type_name: UserDefinedType): |
||||
underlying = type_name.type |
||||
if isinstance(underlying, StructureTopLevel): |
||||
for struct in scope.structures.values(): |
||||
if struct == underlying: |
||||
scope.using_for_directives.add(self._using_for) |
||||
elif isinstance(underlying, EnumTopLevel): |
||||
for enum in scope.enums.values(): |
||||
if enum == underlying: |
||||
scope.using_for_directives.add(self._using_for) |
||||
else: |
||||
LOGGER.error( |
||||
f"Error when propagating global {underlying} {type(underlying)} not a StructTopLevel or EnumTopLevel" |
||||
) |
||||
|
||||
@property |
||||
def is_compact_ast(self) -> bool: |
||||
return self._slither_parser.is_compact_ast |
||||
|
||||
@property |
||||
def compilation_unit(self) -> SlitherCompilationUnit: |
||||
return self._slither_parser.compilation_unit |
||||
|
||||
def get_key(self) -> str: |
||||
return self._slither_parser.get_key() |
||||
|
||||
@property |
||||
def slither_parser(self) -> "SlitherCompilationUnitSolc": |
||||
return self._slither_parser |
||||
|
||||
@property |
||||
def underlying_using_for(self) -> UsingForTopLevel: |
||||
return self._using_for |
@ -0,0 +1,85 @@ |
||||
from pathlib import Path |
||||
from typing import List, Optional, Tuple |
||||
import shutil |
||||
import sys |
||||
import sysconfig |
||||
|
||||
from slither.utils.colors import yellow, green, red |
||||
|
||||
|
||||
def path_is_relative_to(path: Path, relative_to: Path) -> bool: |
||||
""" |
||||
Check if a path is relative to another one. |
||||
|
||||
Compatibility wrapper for Path.is_relative_to |
||||
""" |
||||
if sys.version_info >= (3, 9, 0): |
||||
return path.is_relative_to(relative_to) |
||||
|
||||
path_parts = path.resolve().parts |
||||
relative_to_parts = relative_to.resolve().parts |
||||
|
||||
if len(path_parts) < len(relative_to_parts): |
||||
return False |
||||
|
||||
for (a, b) in zip(path_parts, relative_to_parts): |
||||
if a != b: |
||||
return False |
||||
|
||||
return True |
||||
|
||||
|
||||
def check_path_config(name: str) -> Tuple[bool, Optional[Path], List[Path]]: |
||||
""" |
||||
Check if a given Python binary/script is in PATH. |
||||
:return: Returns if the binary on PATH corresponds to this installation, |
||||
its path (if present), and a list of possible paths where this |
||||
binary might be found. |
||||
""" |
||||
binary_path = shutil.which(name) |
||||
possible_paths = [] |
||||
|
||||
for scheme in sysconfig.get_scheme_names(): |
||||
script_path = Path(sysconfig.get_path("scripts", scheme)) |
||||
purelib_path = Path(sysconfig.get_path("purelib", scheme)) |
||||
script_binary_path = shutil.which(name, path=script_path) |
||||
if script_binary_path is not None: |
||||
possible_paths.append((script_path, purelib_path)) |
||||
|
||||
binary_here = False |
||||
if binary_path is not None: |
||||
binary_path = Path(binary_path) |
||||
this_code = Path(__file__) |
||||
this_binary = list(filter(lambda x: path_is_relative_to(this_code, x[1]), possible_paths)) |
||||
binary_here = len(this_binary) > 0 and all( |
||||
path_is_relative_to(binary_path, script) for script, _ in this_binary |
||||
) |
||||
|
||||
return binary_here, binary_path, list(set(script for script, _ in possible_paths)) |
||||
|
||||
|
||||
def check_slither_path(**_kwargs) -> None: |
||||
binary_here, binary_path, possible_paths = check_path_config("slither") |
||||
show_paths = False |
||||
|
||||
if binary_path: |
||||
print(green(f"`slither` found in PATH at `{binary_path}`.")) |
||||
if binary_here: |
||||
print(green("Its location matches this slither-doctor installation.")) |
||||
else: |
||||
print( |
||||
yellow( |
||||
"This path does not correspond to this slither-doctor installation.\n" |
||||
+ "Double-check the order of directories in PATH if you have several Slither installations." |
||||
) |
||||
) |
||||
show_paths = True |
||||
else: |
||||
print(red("`slither` was not found in PATH.")) |
||||
show_paths = True |
||||
|
||||
if show_paths: |
||||
print() |
||||
print("Consider adding one of the following directories to PATH:") |
||||
for path in possible_paths: |
||||
print(f" * {path}") |
@ -0,0 +1,5 @@ |
||||
# slither-documentation |
||||
|
||||
`slither-documentation` uses [codex](https://beta.openai.com) to generate natspec documenation. |
||||
|
||||
This tool is experimental. See [solmate documentation](https://github.com/montyly/solmate/pull/1) for an example of usage. |
@ -0,0 +1,280 @@ |
||||
import argparse |
||||
import logging |
||||
import uuid |
||||
from typing import Optional, Dict, List |
||||
from crytic_compile import cryticparser |
||||
from slither import Slither |
||||
from slither.core.compilation_unit import SlitherCompilationUnit |
||||
from slither.core.declarations import Function |
||||
|
||||
from slither.formatters.utils.patches import create_patch, apply_patch, create_diff |
||||
from slither.utils import codex |
||||
|
||||
logging.basicConfig() |
||||
logging.getLogger("Slither").setLevel(logging.INFO) |
||||
|
||||
logger = logging.getLogger("Slither") |
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace: |
||||
""" |
||||
Parse the underlying arguments for the program. |
||||
:return: Returns the arguments for the program. |
||||
""" |
||||
parser = argparse.ArgumentParser(description="Demo", usage="slither-documentation filename") |
||||
|
||||
parser.add_argument("project", help="The target directory/Solidity file.") |
||||
|
||||
parser.add_argument( |
||||
"--overwrite", help="Overwrite the files (be careful).", action="store_true", default=False |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"--force-answer-parsing", |
||||
help="Apply heuristics to better parse codex output (might lead to incorrect results)", |
||||
action="store_true", |
||||
default=False, |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"--retry", |
||||
help="Retry failed query (default 1). Each retry increases the temperature by 0.1", |
||||
action="store", |
||||
default=1, |
||||
) |
||||
|
||||
# Add default arguments from crytic-compile |
||||
cryticparser.init(parser) |
||||
|
||||
codex.init_parser(parser, always_enable_codex=True) |
||||
|
||||
return parser.parse_args() |
||||
|
||||
|
||||
def _use_tab(char: str) -> Optional[bool]: |
||||
""" |
||||
Check if the char is a tab |
||||
|
||||
Args: |
||||
char: |
||||
|
||||
Returns: |
||||
|
||||
""" |
||||
if char == " ": |
||||
return False |
||||
if char == "\t": |
||||
return True |
||||
return None |
||||
|
||||
|
||||
def _post_processesing( |
||||
answer: str, starting_column: int, use_tab: Optional[bool], force_and_stopped: bool |
||||
) -> Optional[str]: |
||||
""" |
||||
Clean answers from codex |
||||
|
||||
Args: |
||||
answer: |
||||
starting_column: |
||||
|
||||
Returns: |
||||
|
||||
""" |
||||
if answer.count("/**") != 1: |
||||
return None |
||||
# Sometimes codex will miss the */, even if it finished properly the request |
||||
# In this case, we allow slither-documentation to force the */ |
||||
if answer.count("*/") != 1: |
||||
if force_and_stopped: |
||||
answer += "*/" |
||||
else: |
||||
return None |
||||
if answer.find("/**") > answer.find("*/"): |
||||
return None |
||||
answer = answer[answer.find("/**") : answer.find("*/") + 2] |
||||
answer_lines = answer.splitlines() |
||||
# Add indentation to all the lines, aside the first one |
||||
|
||||
space_char = "\t" if use_tab else " " |
||||
|
||||
if len(answer_lines) > 0: |
||||
answer = ( |
||||
answer_lines[0] |
||||
+ "\n" |
||||
+ "\n".join( |
||||
[space_char * (starting_column - 1) + line for line in answer_lines[1:] if line] |
||||
) |
||||
) |
||||
answer += "\n" + space_char * (starting_column - 1) |
||||
return answer |
||||
return answer_lines[0] |
||||
|
||||
|
||||
def _handle_codex( |
||||
answer: Dict, starting_column: int, use_tab: Optional[bool], force: bool |
||||
) -> Optional[str]: |
||||
if "choices" in answer: |
||||
if answer["choices"]: |
||||
if "text" in answer["choices"][0]: |
||||
has_stopped = answer["choices"][0].get("finish_reason", "") == "stop" |
||||
answer_processed = _post_processesing( |
||||
answer["choices"][0]["text"], starting_column, use_tab, force and has_stopped |
||||
) |
||||
if answer_processed is None: |
||||
return None |
||||
return answer_processed |
||||
return None |
||||
|
||||
|
||||
# pylint: disable=too-many-locals,too-many-arguments |
||||
def _handle_function( |
||||
function: Function, |
||||
overwrite: bool, |
||||
all_patches: Dict, |
||||
logging_file: Optional[str], |
||||
slither: Slither, |
||||
retry: int, |
||||
force: bool, |
||||
) -> bool: |
||||
if ( |
||||
function.source_mapping.is_dependency |
||||
or function.has_documentation |
||||
or function.is_constructor_variables |
||||
): |
||||
return overwrite |
||||
prompt = "Create a natpsec documentation for this solidity code with only notice and dev.\n" |
||||
src_mapping = function.source_mapping |
||||
content = function.compilation_unit.core.source_code[src_mapping.filename.absolute] |
||||
start = src_mapping.start |
||||
end = src_mapping.start + src_mapping.length |
||||
prompt += content[start:end] |
||||
|
||||
use_tab = _use_tab(content[start - 1]) |
||||
if use_tab is None and src_mapping.starting_column > 1: |
||||
logger.info(f"Non standard space indentation found {content[start - 1:end]}") |
||||
if overwrite: |
||||
logger.info("Disable overwrite to avoid mistakes") |
||||
overwrite = False |
||||
|
||||
openai = codex.openai_module() # type: ignore |
||||
if openai is None: |
||||
raise ImportError |
||||
|
||||
if logging_file: |
||||
codex.log_codex(logging_file, "Q: " + prompt) |
||||
|
||||
tentative = 0 |
||||
answer_processed: Optional[str] = None |
||||
while tentative < retry: |
||||
tentative += 1 |
||||
|
||||
answer = openai.Completion.create( # type: ignore |
||||
prompt=prompt, |
||||
model=slither.codex_model, |
||||
temperature=min(slither.codex_temperature + tentative * 0.1, 1), |
||||
max_tokens=slither.codex_max_tokens, |
||||
) |
||||
|
||||
if logging_file: |
||||
codex.log_codex(logging_file, "A: " + str(answer)) |
||||
|
||||
answer_processed = _handle_codex(answer, src_mapping.starting_column, use_tab, force) |
||||
if answer_processed: |
||||
break |
||||
|
||||
logger.info( |
||||
f"Codex could not generate a well formatted answer for {function.canonical_name}" |
||||
) |
||||
logger.info(answer) |
||||
|
||||
if not answer_processed: |
||||
return overwrite |
||||
|
||||
create_patch(all_patches, src_mapping.filename.absolute, start, start, "", answer_processed) |
||||
|
||||
return overwrite |
||||
|
||||
|
||||
def _handle_compilation_unit( |
||||
slither: Slither, |
||||
compilation_unit: SlitherCompilationUnit, |
||||
overwrite: bool, |
||||
force: bool, |
||||
retry: int, |
||||
) -> None: |
||||
logging_file: Optional[str] |
||||
if slither.codex_log: |
||||
logging_file = str(uuid.uuid4()) |
||||
else: |
||||
logging_file = None |
||||
|
||||
for scope in compilation_unit.scopes.values(): |
||||
|
||||
# Dont send tests file |
||||
if ( |
||||
".t.sol" in scope.filename.absolute |
||||
or "mock" in scope.filename.absolute.lower() |
||||
or "test" in scope.filename.absolute.lower() |
||||
): |
||||
continue |
||||
|
||||
functions_target: List[Function] = [] |
||||
|
||||
for contract in scope.contracts.values(): |
||||
functions_target += contract.functions_declared |
||||
|
||||
functions_target += list(scope.functions) |
||||
|
||||
all_patches: Dict = {} |
||||
|
||||
for function in functions_target: |
||||
overwrite = _handle_function( |
||||
function, overwrite, all_patches, logging_file, slither, retry, force |
||||
) |
||||
|
||||
# all_patches["patches"] should have only 1 file |
||||
if "patches" not in all_patches: |
||||
continue |
||||
for file in all_patches["patches"]: |
||||
original_txt = compilation_unit.core.source_code[file].encode("utf8") |
||||
patched_txt = original_txt |
||||
|
||||
patches = all_patches["patches"][file] |
||||
offset = 0 |
||||
patches.sort(key=lambda x: x["start"]) |
||||
|
||||
for patch in patches: |
||||
patched_txt, offset = apply_patch(patched_txt, patch, offset) |
||||
|
||||
if overwrite: |
||||
with open(file, "w", encoding="utf8") as f: |
||||
f.write(patched_txt.decode("utf8")) |
||||
else: |
||||
diff = create_diff(compilation_unit, original_txt, patched_txt, file) |
||||
with open(f"{file}.patch", "w", encoding="utf8") as f: |
||||
f.write(diff) |
||||
|
||||
|
||||
def main() -> None: |
||||
args = parse_args() |
||||
|
||||
logger.info("This tool is a WIP, use it with cautious") |
||||
logger.info("Be aware of OpenAI ToS: https://openai.com/api/policies/terms/") |
||||
slither = Slither(args.project, **vars(args)) |
||||
|
||||
try: |
||||
for compilation_unit in slither.compilation_units: |
||||
_handle_compilation_unit( |
||||
slither, |
||||
compilation_unit, |
||||
args.overwrite, |
||||
args.force_answer_parsing, |
||||
int(args.retry), |
||||
) |
||||
except ImportError: |
||||
pass |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@ |
||||
contract A {} |
@ -0,0 +1,9 @@ |
||||
pragma solidity 0.5.12; |
||||
|
||||
import {A} from "./import.sol"; |
||||
|
||||
contract Z is A { |
||||
function test() public pure returns (uint) { |
||||
return 1; |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
pragma solidity 0.5.12; |
||||
|
||||
import {A as X, A as Y} from "./import.sol"; |
||||
|
||||
contract Z is X { |
||||
function test() public pure returns (uint) { |
||||
return 1; |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
contract Test { |
||||
error myError(); |
||||
} |
||||
|
||||
interface VM { |
||||
function expectRevert(bytes4) external; |
||||
function expectRevert(bytes calldata) external; |
||||
} |
||||
contract A { |
||||
function b(address c) public { |
||||
VM(c).expectRevert(Test.myError.selector); |
||||
} |
||||
} |
@ -0,0 +1,6 @@ |
||||
{ |
||||
"A": {}, |
||||
"Z": { |
||||
"test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n" |
||||
} |
||||
} |
@ -0,0 +1,6 @@ |
||||
{ |
||||
"A": {}, |
||||
"Z": { |
||||
"test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n" |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
{ |
||||
"Test": {}, |
||||
"VM": { |
||||
"expectRevert(bytes4)": "digraph{\n}\n", |
||||
"expectRevert(bytes)": "digraph{\n}\n" |
||||
}, |
||||
"A": { |
||||
"b(address)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue