diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 33a1666b26..6017255b26 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b59aacd66e..f04436bd38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - os: windows-2022 type: truffle steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: @@ -67,7 +67,7 @@ jobs: - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@v22 + uses: cachix/install-nix-action@v23 - name: Set up cachix if: matrix.type == 'dapp' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4cb1adcb1c..e0e303ed84 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,13 +17,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 id: buildx with: install: true @@ -40,14 +40,14 @@ jobs: type=edge - name: GitHub Container Registry Login - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Docker Build and Push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 target: final diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 625cafe4f0..29356c0c6b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v3 - uses: actions/setup-python@v4 diff --git a/.github/workflows/doctor.yml b/.github/workflows/doctor.yml index 85d79f214f..0a0eb896de 100644 --- a/.github/workflows/doctor.yml +++ b/.github/workflows/doctor.yml @@ -29,7 +29,7 @@ jobs: - os: windows-2022 python: 3.8 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 0468b07f8a..5415b6d1be 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 diff --git a/.github/workflows/pip-audit.yml b/.github/workflows/pip-audit.yml index 4fbc1a3fdc..a98f6ab58b 100644 --- a/.github/workflows/pip-audit.yml +++ b/.github/workflows/pip-audit.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b90d490df9..7b4d61e89f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 @@ -47,7 +47,7 @@ jobs: uses: pypa/gh-action-pypi-publish@v1.8.10 - name: sign - uses: sigstore/gh-action-sigstore-python@v2.0.1 + uses: sigstore/gh-action-sigstore-python@v2.1.0 with: inputs: ./dist/*.tar.gz ./dist/*.whl release-signing-artifacts: true diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 8c7e7bce93..7e990371ff 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 533940328a..cb8f0ea6e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: type: ["unit", "integration", "tool"] python: ${{ (github.event_name == 'pull_request' && fromJSON('["3.8", "3.11"]')) || fromJSON('["3.8", "3.9", "3.10", "3.11"]') }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: @@ -100,7 +100,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.8 uses: actions/setup-python@v4 with: diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index d13932b816..3306c134e2 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -5,7 +5,7 @@ cd tests/e2e/solc_parsing/test_data/compile/ || exit # Do not test the evm printer,as it needs a refactoring -ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,human-summary,inheritance,inheritance-graph,loc,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" +ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,martin,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration,ck" # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do diff --git a/slither/__main__.py b/slither/__main__.py index ab15c72d87..72f27d3ad5 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -442,7 +442,7 @@ def parse_args( group_checklist.add_argument( "--checklist-limit", - help="Limite the number of results per detector in the markdown file", + help="Limit the number of results per detector in the markdown file", action="store", default="", ) diff --git a/slither/core/compilation_unit.py b/slither/core/compilation_unit.py index 23387e6fc9..87853aa34a 100644 --- a/slither/core/compilation_unit.py +++ b/slither/core/compilation_unit.py @@ -63,7 +63,7 @@ def __init__(self, core: "SlitherCore", crytic_compilation_unit: CompilationUnit self._pragma_directives: List[Pragma] = [] self._import_directives: List[Import] = [] self._custom_errors: List[CustomErrorTopLevel] = [] - self._user_defined_value_types: Dict[str, TypeAliasTopLevel] = {} + self._type_aliases: Dict[str, TypeAliasTopLevel] = {} self._all_functions: Set[Function] = set() self._all_modifiers: Set[Modifier] = set() @@ -251,8 +251,8 @@ def custom_errors(self) -> List[CustomErrorTopLevel]: return self._custom_errors @property - def user_defined_value_types(self) -> Dict[str, TypeAliasTopLevel]: - return self._user_defined_value_types + def type_aliases(self) -> Dict[str, TypeAliasTopLevel]: + return self._type_aliases # endregion ################################################################################### diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index 5e3ab4571e..2261a64ecf 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -45,6 +45,7 @@ from slither.core.compilation_unit import SlitherCompilationUnit from slither.core.scope.scope import FileScope from slither.core.cfg.node import Node + from slither.core.solidity_types import TypeAliasContract LOGGER = logging.getLogger("Contract") @@ -81,6 +82,7 @@ def __init__(self, compilation_unit: "SlitherCompilationUnit", scope: "FileScope self._functions: Dict[str, "FunctionContract"] = {} self._linearizedBaseContracts: List[int] = [] self._custom_errors: Dict[str, "CustomErrorContract"] = {} + self._type_aliases: Dict[str, "TypeAliasContract"] = {} # The only str is "*" self._using_for: Dict[USING_FOR_KEY, USING_FOR_ITEM] = {} @@ -364,6 +366,38 @@ def custom_errors_declared(self) -> List["CustomErrorContract"]: def custom_errors_as_dict(self) -> Dict[str, "CustomErrorContract"]: return self._custom_errors + # endregion + ################################################################################### + ################################################################################### + # region Custom Errors + ################################################################################### + ################################################################################### + + @property + def type_aliases(self) -> List["TypeAliasContract"]: + """ + list(TypeAliasContract): List of the contract's custom errors + """ + return list(self._type_aliases.values()) + + @property + def type_aliases_inherited(self) -> List["TypeAliasContract"]: + """ + list(TypeAliasContract): List of the inherited custom errors + """ + return [s for s in self.type_aliases if s.contract != self] + + @property + def type_aliases_declared(self) -> List["TypeAliasContract"]: + """ + list(TypeAliasContract): List of the custom errors declared within the contract (not inherited) + """ + return [s for s in self.type_aliases if s.contract == self] + + @property + def type_aliases_as_dict(self) -> Dict[str, "TypeAliasContract"]: + return self._type_aliases + # endregion ################################################################################### ################################################################################### diff --git a/slither/core/expressions/call_expression.py b/slither/core/expressions/call_expression.py index 6708dda7e2..ec2b2cfceb 100644 --- a/slither/core/expressions/call_expression.py +++ b/slither/core/expressions/call_expression.py @@ -4,12 +4,32 @@ class CallExpression(Expression): # pylint: disable=too-many-instance-attributes - def __init__(self, called: Expression, arguments: List[Any], type_call: str) -> None: + def __init__( + self, + called: Expression, + arguments: List[Any], + type_call: str, + names: Optional[List[str]] = None, + ) -> None: + """ + #### Parameters + called - + The expression denoting the function to be called + arguments - + List of argument expressions + type_call - + A string formatting of the called function's return type + names - + For calls with named fields, list fields in call order. + For calls without named fields, None. + """ assert isinstance(called, Expression) + assert (names is None) or isinstance(names, list) super().__init__() self._called: Expression = called self._arguments: List[Expression] = arguments self._type_call: str = type_call + self._names: Optional[List[str]] = names # gas and value are only available if the syntax is {gas: , value: } # For the .gas().value(), the member are considered as function call # And converted later to the correct info (convert.py) @@ -17,6 +37,14 @@ def __init__(self, called: Expression, arguments: List[Any], type_call: str) -> self._value: Optional[Expression] = None self._salt: Optional[Expression] = None + @property + def names(self) -> Optional[List[str]]: + """ + For calls with named fields, list fields in call order. + For calls without named fields, None. + """ + return self._names + @property def call_value(self) -> Optional[Expression]: return self._value @@ -62,4 +90,9 @@ def __str__(self) -> str: if gas or value or salt: options = [gas, value, salt] txt += "{" + ",".join([o for o in options if o != ""]) + "}" - return txt + "(" + ",".join([str(a) for a in self._arguments]) + ")" + args = ( + "{" + ",".join([f"{n}:{str(a)}" for (a, n) in zip(self._arguments, self._names)]) + "}" + if self._names is not None + else ",".join([str(a) for a in self._arguments]) + ) + return txt + "(" + args + ")" diff --git a/slither/core/scope/scope.py b/slither/core/scope/scope.py index cafeb3585d..937a051361 100644 --- a/slither/core/scope/scope.py +++ b/slither/core/scope/scope.py @@ -52,7 +52,7 @@ def __init__(self, filename: Filename) -> None: # User defined types # Name -> type alias - self.user_defined_types: Dict[str, TypeAlias] = {} + self.type_aliases: Dict[str, TypeAlias] = {} def add_accesible_scopes(self) -> bool: """ @@ -95,8 +95,8 @@ def add_accesible_scopes(self) -> bool: if not _dict_contain(new_scope.renaming, self.renaming): self.renaming.update(new_scope.renaming) learn_something = True - if not _dict_contain(new_scope.user_defined_types, self.user_defined_types): - self.user_defined_types.update(new_scope.user_defined_types) + if not _dict_contain(new_scope.type_aliases, self.type_aliases): + self.type_aliases.update(new_scope.type_aliases) learn_something = True return learn_something diff --git a/slither/core/variables/__init__.py b/slither/core/variables/__init__.py index 638f0f3a4e..53872853aa 100644 --- a/slither/core/variables/__init__.py +++ b/slither/core/variables/__init__.py @@ -1,2 +1,8 @@ from .state_variable import StateVariable from .variable import Variable +from .local_variable_init_from_tuple import LocalVariableInitFromTuple +from .local_variable import LocalVariable +from .top_level_variable import TopLevelVariable +from .event_variable import EventVariable +from .function_type_variable import FunctionTypeVariable +from .structure_variable import StructureVariable diff --git a/slither/core/variables/local_variable.py b/slither/core/variables/local_variable.py index 427f004082..9baf804457 100644 --- a/slither/core/variables/local_variable.py +++ b/slither/core/variables/local_variable.py @@ -2,7 +2,6 @@ from slither.core.variables.variable import Variable from slither.core.solidity_types.user_defined_type import UserDefinedType -from slither.core.solidity_types.array_type import ArrayType from slither.core.solidity_types.mapping_type import MappingType from slither.core.solidity_types.elementary_type import ElementaryType @@ -51,6 +50,9 @@ def is_storage(self) -> bool: Returns: (bool) """ + # pylint: disable=import-outside-toplevel + from slither.core.solidity_types.array_type import ArrayType + if self.location == "memory": return False if self.location == "calldata": diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 57f44bc56e..fab9562d20 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -92,3 +92,8 @@ from .operations.cache_array_length import CacheArrayLength from .statements.incorrect_using_for import IncorrectUsingFor from .operations.encode_packed import EncodePackedCollision +from .assembly.incorrect_return import IncorrectReturn +from .assembly.return_instead_of_leave import ReturnInsteadOfLeave +from .operations.incorrect_exp import IncorrectOperatorExponentiation +from .statements.tautological_compare import TautologicalCompare +from .statements.return_bomb import ReturnBomb diff --git a/slither/detectors/assembly/incorrect_return.py b/slither/detectors/assembly/incorrect_return.py new file mode 100644 index 0000000000..f5f0a98d9d --- /dev/null +++ b/slither/detectors/assembly/incorrect_return.py @@ -0,0 +1,91 @@ +from typing import List, Optional + +from slither.core.declarations import SolidityFunction, Function +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.slithir.operations import SolidityCall +from slither.utils.output import Output + + +def _assembly_node(function: Function) -> Optional[SolidityCall]: + """ + Check if there is a node that use return in assembly + + Args: + function: + + Returns: + + """ + + for ir in function.all_slithir_operations(): + if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( + "return(uint256,uint256)" + ): + return ir + return None + + +class IncorrectReturn(AbstractDetector): + """ + Check for cases where a return(a,b) is used in an assembly function + """ + + ARGUMENT = "incorrect-return" + HELP = "If a `return` is incorrectly used in assembly mode." + IMPACT = DetectorClassification.HIGH + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" + + WIKI_TITLE = "Incorrect return in assembly" + WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution." + WIKI_EXPLOIT_SCENARIO = """ +```solidity +contract C { + function f() internal returns (uint a, uint b) { + assembly { + return (5, 6) + } + } + + function g() returns (bool){ + f(); + return true; + } +} +``` +The return statement in `f` will cause execution in `g` to halt. +The function will return 6 bytes starting from offset 5, instead of returning a boolean.""" + + WIKI_RECOMMENDATION = "Use the `leave` statement." + + # pylint: disable=too-many-nested-blocks + def _detect(self) -> List[Output]: + results: List[Output] = [] + for c in self.contracts: + for f in c.functions_and_modifiers_declared: + + for node in f.nodes: + if node.sons: + for function_called in node.internal_calls: + if isinstance(function_called, Function): + found = _assembly_node(function_called) + if found: + + info: DETECTOR_INFO = [ + f, + " calls ", + function_called, + " which halt the execution ", + found.node, + "\n", + ] + json = self.generate_result(info) + + results.append(json) + + return results diff --git a/slither/detectors/assembly/return_instead_of_leave.py b/slither/detectors/assembly/return_instead_of_leave.py new file mode 100644 index 0000000000..a1591d834b --- /dev/null +++ b/slither/detectors/assembly/return_instead_of_leave.py @@ -0,0 +1,68 @@ +from typing import List + +from slither.core.declarations import SolidityFunction, Function +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.slithir.operations import SolidityCall +from slither.utils.output import Output + + +class ReturnInsteadOfLeave(AbstractDetector): + """ + Check for cases where a return(a,b) is used in an assembly function that also returns two variables + """ + + ARGUMENT = "return-leave" + HELP = "If a `return` is used instead of a `leave`." + IMPACT = DetectorClassification.HIGH + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" + + WIKI_TITLE = "Return instead of leave in assembly" + WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used." + WIKI_EXPLOIT_SCENARIO = """ +```solidity +contract C { + function f() internal returns (uint a, uint b) { + assembly { + return (5, 6) + } + } + +} +``` +The function will halt the execution, instead of returning a two uint.""" + + WIKI_RECOMMENDATION = "Use the `leave` statement." + + def _check_function(self, f: Function) -> List[Output]: + results: List[Output] = [] + + for node in f.nodes: + for ir in node.irs: + if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( + "return(uint256,uint256)" + ): + info: DETECTOR_INFO = [f, " contains an incorrect call to return: ", node, "\n"] + json = self.generate_result(info) + + results.append(json) + return results + + def _detect(self) -> List[Output]: + results: List[Output] = [] + for c in self.contracts: + for f in c.functions_declared: + + if ( + len(f.returns) == 2 + and f.contains_assembly + and f.visibility not in ["public", "external"] + ): + results += self._check_function(f) + + return results diff --git a/slither/detectors/operations/incorrect_exp.py b/slither/detectors/operations/incorrect_exp.py new file mode 100644 index 0000000000..6188f5cb16 --- /dev/null +++ b/slither/detectors/operations/incorrect_exp.py @@ -0,0 +1,93 @@ +""" +Module detecting incorrect operator usage for exponentiation where bitwise xor '^' is used instead of '**' +""" +from typing import Tuple, List, Union + +from slither.core.cfg.node import Node +from slither.core.declarations import Contract, Function +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.slithir.operations import Binary, BinaryType, Operation +from slither.slithir.utils.utils import RVALUE +from slither.slithir.variables.constant import Constant +from slither.utils.output import Output + + +def _is_constant_candidate(var: Union[RVALUE, Function]) -> bool: + """ + Check if the variable is a constant. + Do not consider variable that are expressed with hexadecimal. + Something like 2^0xf is likely to be a correct bitwise operator + :param var: + :return: + """ + return isinstance(var, Constant) and not var.original_value.startswith("0x") + + +def _is_bitwise_xor_on_constant(ir: Operation) -> bool: + return ( + isinstance(ir, Binary) + and ir.type == BinaryType.CARET + and (_is_constant_candidate(ir.variable_left) or _is_constant_candidate(ir.variable_right)) + ) + + +def _detect_incorrect_operator(contract: Contract) -> List[Tuple[Function, Node]]: + ret: List[Tuple[Function, Node]] = [] + f: Function + for f in contract.functions + contract.modifiers: # type:ignore + # Heuristic: look for binary expressions with ^ operator where at least one of the operands is a constant, and + # the constant is not in hex, because hex typically is used with bitwise xor and not exponentiation + nodes = [node for node in f.nodes for ir in node.irs if _is_bitwise_xor_on_constant(ir)] + for node in nodes: + ret.append((f, node)) + return ret + + +# pylint: disable=too-few-public-methods +class IncorrectOperatorExponentiation(AbstractDetector): + """ + Incorrect operator usage of bitwise xor mistaking it for exponentiation + """ + + ARGUMENT = "incorrect-exp" + HELP = "Incorrect exponentiation" + IMPACT = DetectorClassification.HIGH + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation" + + WIKI_TITLE = "Incorrect exponentiation" + WIKI_DESCRIPTION = "Detect use of bitwise `xor ^` instead of exponential `**`" + WIKI_EXPLOIT_SCENARIO = """ +```solidity +contract Bug{ + uint UINT_MAX = 2^256 - 1; + ... +} +``` +Alice deploys a contract in which `UINT_MAX` incorrectly uses `^` operator instead of `**` for exponentiation""" + + WIKI_RECOMMENDATION = "Use the correct operator `**` for exponentiation." + + def _detect(self) -> List[Output]: + """Detect the incorrect operator usage for exponentiation where bitwise xor ^ is used instead of ** + + Returns: + list: (function, node) + """ + results: List[Output] = [] + for c in self.compilation_unit.contracts_derived: + res = _detect_incorrect_operator(c) + for (func, node) in res: + info: DETECTOR_INFO = [ + func, + " has bitwise-xor operator ^ instead of the exponentiation operator **: \n", + ] + info += ["\t - ", node, "\n"] + results.append(self.generate_result(info)) + + return results diff --git a/slither/detectors/statements/mapping_deletion.py b/slither/detectors/statements/mapping_deletion.py index 4cdac72400..0940d5a07b 100644 --- a/slither/detectors/statements/mapping_deletion.py +++ b/slither/detectors/statements/mapping_deletion.py @@ -6,6 +6,7 @@ from slither.core.cfg.node import Node from slither.core.declarations import Structure from slither.core.declarations.contract import Contract +from slither.core.variables.variable import Variable from slither.core.declarations.function_contract import FunctionContract from slither.core.solidity_types import MappingType, UserDefinedType from slither.detectors.abstract_detector import ( @@ -69,14 +70,25 @@ def detect_mapping_deletion( for ir in node.irs: if isinstance(ir, Delete): value = ir.variable - if isinstance(value.type, UserDefinedType) and isinstance( - value.type.type, Structure - ): - st = value.type.type - if any(isinstance(e.type, MappingType) for e in st.elems.values()): - ret.append((f, st, node)) + MappingDeletionDetection.check_if_mapping(value, ret, f, node) + return ret + @staticmethod + def check_if_mapping( + value: Variable, + ret: List[Tuple[FunctionContract, Structure, Node]], + f: FunctionContract, + node: Node, + ): + if isinstance(value.type, UserDefinedType) and isinstance(value.type.type, Structure): + st = value.type.type + if any(isinstance(e.type, MappingType) for e in st.elems.values()): + ret.append((f, st, node)) + return + for e in st.elems.values(): + MappingDeletionDetection.check_if_mapping(e, ret, f, node) + def _detect(self) -> List[Output]: """Detect mapping deletion diff --git a/slither/detectors/statements/return_bomb.py b/slither/detectors/statements/return_bomb.py new file mode 100644 index 0000000000..8b6cd07a29 --- /dev/null +++ b/slither/detectors/statements/return_bomb.py @@ -0,0 +1,123 @@ +from typing import List + +from slither.core.cfg.node import Node +from slither.core.declarations import Contract +from slither.core.declarations.function import Function +from slither.core.solidity_types import Type +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.slithir.operations import LowLevelCall, HighLevelCall +from slither.analyses.data_dependency.data_dependency import is_tainted +from slither.utils.output import Output + + +class ReturnBomb(AbstractDetector): + + ARGUMENT = "return-bomb" + HELP = "A low level callee may consume all callers gas unexpectedly." + IMPACT = DetectorClassification.LOW + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-bomb" + + WIKI_TITLE = "Return Bomb" + WIKI_DESCRIPTION = "A low level callee may consume all callers gas unexpectedly." + WIKI_EXPLOIT_SCENARIO = """ +```solidity +//Modified from https://github.com/nomad-xyz/ExcessivelySafeCall +contract BadGuy { + function youveActivateMyTrapCard() external pure returns (bytes memory) { + assembly{ + revert(0, 1000000) + } + } +} + +contract Mark { + function oops(address badGuy) public{ + bool success; + bytes memory ret; + + // Mark pays a lot of gas for this copy + //(success, ret) = badGuy.call{gas:10000}( + (success, ret) = badGuy.call( + abi.encodeWithSelector( + BadGuy.youveActivateMyTrapCard.selector + ) + ); + + // Mark may OOG here, preventing local state changes + //importantCleanup(); + } +} + +``` +After Mark calls BadGuy bytes are copied from returndata to memory, the memory expansion cost is paid. This means that when using a standard solidity call, the callee can "returnbomb" the caller, imposing an arbitrary gas cost. +Callee unexpectedly makes the caller OOG. +""" + + WIKI_RECOMMENDATION = "Avoid unlimited implicit decoding of returndata." + + @staticmethod + def is_dynamic_type(ty: Type) -> bool: + # ty.is_dynamic ? + name = str(ty) + if "[]" in name or name in ("bytes", "string"): + return True + return False + + def get_nodes_for_function(self, function: Function, contract: Contract) -> List[Node]: + nodes = [] + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, (HighLevelCall, LowLevelCall)): + if not is_tainted(ir.destination, contract): # type:ignore + # Only interested if the target address is controlled/tainted + continue + + if isinstance(ir, HighLevelCall) and isinstance(ir.function, Function): + # in normal highlevel calls return bombs are _possible_ + # if the return type is dynamic and the caller tries to copy and decode large data + has_dyn = False + if ir.function.return_type: + has_dyn = any( + self.is_dynamic_type(ty) for ty in ir.function.return_type + ) + + if not has_dyn: + continue + + # If a gas budget was specified then the + # user may not know about the return bomb + if ir.call_gas is None: + # if a gas budget was NOT specified then the caller + # may already suspect the call may spend all gas? + continue + + nodes.append(node) + # TODO: check that there is some state change after the call + + return nodes + + def _detect(self) -> List[Output]: + results = [] + + for contract in self.compilation_unit.contracts: + for function in contract.functions_declared: + nodes = self.get_nodes_for_function(function, contract) + if nodes: + info: DETECTOR_INFO = [ + function, + " tries to limit the gas of an external call that controls implicit decoding\n", + ] + + for node in sorted(nodes, key=lambda x: x.node_id): + info += ["\t", node, "\n"] + + res = self.generate_result(info) + results.append(res) + + return results diff --git a/slither/detectors/statements/tautological_compare.py b/slither/detectors/statements/tautological_compare.py new file mode 100644 index 0000000000..0dc34b582f --- /dev/null +++ b/slither/detectors/statements/tautological_compare.py @@ -0,0 +1,69 @@ +from typing import List +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.slithir.operations import ( + Binary, + BinaryType, +) + +from slither.core.declarations import Function +from slither.utils.output import Output + + +class TautologicalCompare(AbstractDetector): + """ + Same variable comparison detector + """ + + ARGUMENT = "tautological-compare" + HELP = "Comparing a variable to itself always returns true or false, depending on comparison" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.HIGH + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#tautological-compare" + + WIKI_TITLE = "Tautological compare" + WIKI_DESCRIPTION = "A variable compared to itself is probably an error as it will always return `true` for `==`, `>=`, `<=` and always `false` for `<`, `>` and `!=`." + WIKI_EXPLOIT_SCENARIO = """ +```solidity + function check(uint a) external returns(bool){ + return (a >= a); + } +``` +`check` always return true.""" + + WIKI_RECOMMENDATION = "Remove comparison or compare to different value." + + def _check_function(self, f: Function) -> List[Output]: + affected_nodes = set() + for node in f.nodes: + for ir in node.irs: + if isinstance(ir, Binary): + if ir.type in [ + BinaryType.GREATER, + BinaryType.GREATER_EQUAL, + BinaryType.LESS, + BinaryType.LESS_EQUAL, + BinaryType.EQUAL, + BinaryType.NOT_EQUAL, + ]: + if ir.variable_left == ir.variable_right: + affected_nodes.add(node) + + results = [] + for n in affected_nodes: + info: DETECTOR_INFO = [f, " compares a variable to itself:\n\t", n, "\n"] + res = self.generate_result(info) + results.append(res) + return results + + def _detect(self): + results = [] + + for f in self.compilation_unit.functions_and_modifiers: + results.extend(self._check_function(f)) + + return results diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 5555fe7082..3edd5325b6 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -9,6 +9,8 @@ from .summary.slithir import PrinterSlithIR from .summary.slithir_ssa import PrinterSlithIRSSA from .summary.human_summary import PrinterHumanSummary +from .summary.ck import CK +from .summary.halstead import Halstead from .functions.cfg import CFG from .summary.function_ids import FunctionIds from .summary.variable_order import VariableOrder @@ -21,3 +23,4 @@ from .summary.when_not_paused import PrinterWhenNotPaused from .summary.declaration import Declaration from .functions.dominator import Dominator +from .summary.martin import Martin diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 25e0968cdb..73a9c5619d 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -126,15 +126,24 @@ def _extract_constant_functions(slither: SlitherCore) -> Dict[str, List[str]]: return ret -def _extract_assert(slither: SlitherCore) -> Dict[str, List[str]]: - ret: Dict[str, List[str]] = {} +def _extract_assert(slither: SlitherCore) -> Dict[str, Dict[str, List[Dict]]]: + """ + Return the list of contract -> function name -> List(source mapping of the assert)) + + Args: + slither: + + Returns: + + """ + ret: Dict[str, Dict[str, List[Dict]]] = {} for contract in slither.contracts: - functions_using_assert = [] + functions_using_assert: Dict[str, List[Dict]] = defaultdict(list) for f in contract.functions_entry_points: - for v in f.all_solidity_calls(): - if v == SolidityFunction("assert(bool)"): - functions_using_assert.append(_get_name(f)) - break + for node in f.all_nodes(): + if SolidityFunction("assert(bool)") in node.solidity_calls and node.source_mapping: + func_name = _get_name(f) + functions_using_assert[func_name].append(node.source_mapping.to_json()) if functions_using_assert: ret[contract.name] = functions_using_assert return ret diff --git a/slither/printers/summary/ck.py b/slither/printers/summary/ck.py new file mode 100644 index 0000000000..78da23756b --- /dev/null +++ b/slither/printers/summary/ck.py @@ -0,0 +1,58 @@ +""" + CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. + These metrics are used to measure the complexity of a class. + https://en.wikipedia.org/wiki/Programming_complexity + + - Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. + - Number of Children (NOC) is a metric that measures the number of children a class has. + - Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. + - Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. + + Not implemented: + - Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. + - Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. + + During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated. + These are also included in the output: + - State variables: total number of state variables + - Constants: total number of constants + - Immutables: total number of immutables + - Public: total number of public functions + - External: total number of external functions + - Internal: total number of internal functions + - Private: total number of private functions + - Mutating: total number of state mutating functions + - View: total number of view functions + - Pure: total number of pure functions + - External mutating: total number of external mutating functions + - No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers + - No modifiers: total number of functions without modifiers + - Ext calls: total number of external calls + +""" +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.ck import CKMetrics +from slither.utils.output import Output + + +class CK(AbstractPrinter): + ARGUMENT = "ck" + HELP = "Chidamber and Kemerer (CK) complexity metrics and related function attributes" + + WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#ck" + + def output(self, _filename: str) -> Output: + if len(self.contracts) == 0: + return self.generate_output("No contract found") + + ck = CKMetrics(self.contracts) + + res = self.generate_output(ck.full_text) + res.add_pretty_table(ck.auxiliary1.pretty_table, ck.auxiliary1.title) + res.add_pretty_table(ck.auxiliary2.pretty_table, ck.auxiliary2.title) + res.add_pretty_table(ck.auxiliary3.pretty_table, ck.auxiliary3.title) + res.add_pretty_table(ck.auxiliary4.pretty_table, ck.auxiliary4.title) + res.add_pretty_table(ck.core.pretty_table, ck.core.title) + self.info(ck.full_text) + + return res diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py new file mode 100644 index 0000000000..d3c3557db9 --- /dev/null +++ b/slither/printers/summary/halstead.py @@ -0,0 +1,49 @@ +""" + Halstead complexity metrics + https://en.wikipedia.org/wiki/Halstead_complexity_measures + + 12 metrics based on the number of unique operators and operands: + + Core metrics: + n1 = the number of distinct operators + n2 = the number of distinct operands + N1 = the total number of operators + N2 = the total number of operands + + Extended metrics1: + n = n1 + n2 # Program vocabulary + N = N1 + N2 # Program length + S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length + V = N * log2(n) # Volume + + Extended metrics2: + D = (n1 / 2) * (N2 / n2) # Difficulty + E = D * V # Effort + T = E / 18 seconds # Time required to program + B = (E^(2/3)) / 3000 # Number of delivered bugs + +""" +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.halstead import HalsteadMetrics +from slither.utils.output import Output + + +class Halstead(AbstractPrinter): + ARGUMENT = "halstead" + HELP = "Computes the Halstead complexity metrics for each contract" + + WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#halstead" + + def output(self, _filename: str) -> Output: + if len(self.contracts) == 0: + return self.generate_output("No contract found") + + halstead = HalsteadMetrics(self.contracts) + + res = self.generate_output(halstead.full_text) + res.add_pretty_table(halstead.core.pretty_table, halstead.core.title) + res.add_pretty_table(halstead.extended1.pretty_table, halstead.extended1.title) + res.add_pretty_table(halstead.extended2.pretty_table, halstead.extended2.title) + self.info(halstead.full_text) + + return res diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py new file mode 100644 index 0000000000..a0f1bbfcb1 --- /dev/null +++ b/slither/printers/summary/martin.py @@ -0,0 +1,32 @@ +""" + Robert "Uncle Bob" Martin - Agile software metrics + https://en.wikipedia.org/wiki/Software_package_metrics + + Efferent Coupling (Ce): Number of contracts that the contract depends on + Afferent Coupling (Ca): Number of contracts that depend on a contract + Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) + Abstractness (A): Number of abstract contracts / total number of contracts + Distance from the Main Sequence (D): abs(A + I - 1) + +""" +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.martin import MartinMetrics +from slither.utils.output import Output + + +class Martin(AbstractPrinter): + ARGUMENT = "martin" + HELP = "Martin agile software metrics (Ca, Ce, I, A, D)" + + WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#martin" + + def output(self, _filename: str) -> Output: + if len(self.contracts) == 0: + return self.generate_output("No contract found") + + martin = MartinMetrics(self.contracts) + + res = self.generate_output(martin.full_text) + res.add_pretty_table(martin.core.pretty_table, martin.core.title) + self.info(martin.full_text) + return res diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 91fe30da7f..4411e3505c 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -385,6 +385,70 @@ def integrate_value_gas(result: List[Operation]) -> List[Operation]: ################################################################################### +def get_declared_param_names( + ins: Union[ + NewStructure, + NewContract, + InternalCall, + LibraryCall, + HighLevelCall, + InternalDynamicCall, + EventCall, + ] +) -> Optional[List[str]]: + """ + Given a call operation, return the list of parameter names, in the order + listed in the function declaration. + #### Parameters + ins - + The call instruction + #### Possible Returns + List[str] - + A list of the parameters in declaration order + None - + Workaround: Unable to obtain list of parameters in declaration order + """ + if isinstance(ins, NewStructure): + return [x.name for x in ins.structure.elems_ordered if not isinstance(x.type, MappingType)] + if isinstance(ins, (InternalCall, LibraryCall, HighLevelCall)): + if isinstance(ins.function, Function): + return [p.name for p in ins.function.parameters] + return None + if isinstance(ins, InternalDynamicCall): + return [p.name for p in ins.function_type.params] + + assert isinstance(ins, (EventCall, NewContract)) + return None + + +def reorder_arguments( + args: List[Variable], call_names: List[str], decl_names: List[str] +) -> List[Variable]: + """ + Reorder named struct constructor arguments so that they match struct declaration ordering rather + than call ordering + E.g. for `struct S { int x; int y; }` we reorder `S({y : 2, x : 3})` to `S(3, 2)` + #### Parameters + args - + Arguments to constructor call, in call order + names - + Parameter names in call order + decl_names - + Parameter names in declaration order + #### Returns + Reordered arguments to constructor call, now in declaration order + """ + assert len(args) == len(call_names) + assert len(call_names) == len(decl_names) + + args_ret = [] + for n in decl_names: + ind = call_names.index(n) + args_ret.append(args[ind]) + + return args_ret + + def propagate_type_and_convert_call(result: List[Operation], node: "Node") -> List[Operation]: """ Propagate the types variables and convert tmp call to real call operation @@ -434,6 +498,23 @@ def propagate_type_and_convert_call(result: List[Operation], node: "Node") -> Li if ins.call_id in calls_gas and isinstance(ins, (HighLevelCall, InternalDynamicCall)): ins.call_gas = calls_gas[ins.call_id] + if isinstance(ins, Call) and (ins.names is not None): + assert isinstance( + ins, + ( + NewStructure, + NewContract, + InternalCall, + LibraryCall, + HighLevelCall, + InternalDynamicCall, + EventCall, + ), + ) + decl_param_names = get_declared_param_names(ins) + if decl_param_names is not None: + call_data = reorder_arguments(call_data, ins.names, decl_param_names) + if isinstance(ins, (Call, NewContract, NewStructure)): # We might have stored some arguments for libraries if ins.arguments: @@ -857,7 +938,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call, if isinstance(ins.ori.variable_left, Contract): st = ins.ori.variable_left.get_structure_from_name(ins.ori.variable_right) if st: - op = NewStructure(st, ins.lvalue) + op = NewStructure(st, ins.lvalue, names=ins.names) op.set_expression(ins.expression) op.call_id = ins.call_id return op @@ -894,6 +975,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call, ins.nbr_arguments, ins.lvalue, ins.type_call, + names=ins.names, ) libcall.set_expression(ins.expression) libcall.call_id = ins.call_id @@ -952,6 +1034,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call, len(lib_func.parameters), ins.lvalue, "d", + names=ins.names, ) lib_call.set_expression(ins.expression) lib_call.set_node(ins.node) @@ -1033,6 +1116,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call, ins.nbr_arguments, ins.lvalue, ins.type_call, + names=ins.names, ) msgcall.call_id = ins.call_id @@ -1084,7 +1168,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call, return n if isinstance(ins.called, Structure): - op = NewStructure(ins.called, ins.lvalue) + op = NewStructure(ins.called, ins.lvalue, names=ins.names) op.set_expression(ins.expression) op.call_id = ins.call_id op.set_expression(ins.expression) @@ -1108,7 +1192,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call, if len(ins.called.constructor.parameters) != ins.nbr_arguments: return Nop() internalcall = InternalCall( - ins.called.constructor, ins.nbr_arguments, ins.lvalue, ins.type_call + ins.called.constructor, ins.nbr_arguments, ins.lvalue, ins.type_call, ins.names ) internalcall.call_id = ins.call_id internalcall.set_expression(ins.expression) @@ -1214,7 +1298,12 @@ def convert_to_solidity_func( and len(new_ir.arguments) == 2 and isinstance(new_ir.arguments[1], list) ): - types = list(new_ir.arguments[1]) + types = [] + for arg_type in new_ir.arguments[1]: + decode_type = arg_type + if isinstance(decode_type, (Structure, Enum, Contract)): + decode_type = UserDefinedType(decode_type) + types.append(decode_type) new_ir.lvalue.set_type(types) # abi.decode where the type to decode is a singleton # abi.decode(a, (uint)) @@ -1444,6 +1533,7 @@ def look_for_library_or_top_level( ir.nbr_arguments, ir.lvalue, ir.type_call, + names=ir.names, ) lib_call.set_expression(ir.expression) lib_call.set_node(ir.node) @@ -1861,7 +1951,7 @@ def convert_constant_types(irs: List[Operation]) -> None: if isinstance(ir.lvalue.type.type, ElementaryType): if ir.lvalue.type.type.type in ElementaryTypeInt: for r in ir.read: - if r.type.type not in ElementaryTypeInt: + if r.type.type.type not in ElementaryTypeInt: r.set_type(ElementaryType(ir.lvalue.type.type.type)) was_changed = True diff --git a/slither/slithir/operations/call.py b/slither/slithir/operations/call.py index 816c56e1d9..a4f9f6d722 100644 --- a/slither/slithir/operations/call.py +++ b/slither/slithir/operations/call.py @@ -6,9 +6,25 @@ class Call(Operation): - def __init__(self) -> None: + def __init__(self, names: Optional[List[str]] = None) -> None: + """ + #### Parameters + names - + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ + assert (names is None) or isinstance(names, list) super().__init__() self._arguments: List[Variable] = [] + self._names = names + + @property + def names(self) -> Optional[List[str]]: + """ + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ + return self._names @property def arguments(self) -> List[Variable]: diff --git a/slither/slithir/operations/high_level_call.py b/slither/slithir/operations/high_level_call.py index 5d654fc800..c60443f199 100644 --- a/slither/slithir/operations/high_level_call.py +++ b/slither/slithir/operations/high_level_call.py @@ -28,11 +28,18 @@ def __init__( nbr_arguments: int, result: Optional[Union[TemporaryVariable, TupleVariable, TemporaryVariableSSA]], type_call: str, + names: Optional[List[str]] = None, ) -> None: + """ + #### Parameters + names - + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ assert isinstance(function_name, Constant) assert is_valid_lvalue(result) or result is None self._check_destination(destination) - super().__init__() + super().__init__(names=names) # Contract is only possible for library call, which inherits from highlevelcall self._destination: Union[Variable, SolidityVariable, Contract] = destination # type: ignore self._function_name = function_name diff --git a/slither/slithir/operations/init_array.py b/slither/slithir/operations/init_array.py index e0754c7705..ec2a63ef00 100644 --- a/slither/slithir/operations/init_array.py +++ b/slither/slithir/operations/init_array.py @@ -44,4 +44,4 @@ def convert(elem): return f"{elem}({elem.type})" init_values = convert(self.init_values) - return f"{self.lvalue}({self.lvalue.type}) = {init_values}" + return f"{self.lvalue}({self.lvalue.type}) = {init_values}" diff --git a/slither/slithir/operations/internal_call.py b/slither/slithir/operations/internal_call.py index 1983b885fe..06e75136e6 100644 --- a/slither/slithir/operations/internal_call.py +++ b/slither/slithir/operations/internal_call.py @@ -20,8 +20,16 @@ def __init__( Union[TupleVariableSSA, TemporaryVariableSSA, TupleVariable, TemporaryVariable] ], type_call: str, + names: Optional[List[str]] = None, ) -> None: - super().__init__() + # pylint: disable=too-many-arguments + """ + #### Parameters + names - + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ + super().__init__(names=names) self._contract_name = "" if isinstance(function, Function): self._function: Optional[Function] = function diff --git a/slither/slithir/operations/new_array.py b/slither/slithir/operations/new_array.py index 39b989459f..917bb11ee9 100644 --- a/slither/slithir/operations/new_array.py +++ b/slither/slithir/operations/new_array.py @@ -33,4 +33,4 @@ def read(self) -> List["Constant"]: def __str__(self): args = [str(a) for a in self.arguments] lvalue = self.lvalue - return f"{lvalue}{lvalue.type}) = new {self.array_type}({','.join(args)})" + return f"{lvalue}({lvalue.type}) = new {self.array_type}({','.join(args)})" diff --git a/slither/slithir/operations/new_contract.py b/slither/slithir/operations/new_contract.py index 518a097cb5..9ed00b1ac0 100644 --- a/slither/slithir/operations/new_contract.py +++ b/slither/slithir/operations/new_contract.py @@ -12,11 +12,20 @@ class NewContract(Call, OperationWithLValue): # pylint: disable=too-many-instance-attributes def __init__( - self, contract_name: Constant, lvalue: Union[TemporaryVariableSSA, TemporaryVariable] + self, + contract_name: Constant, + lvalue: Union[TemporaryVariableSSA, TemporaryVariable], + names: Optional[List[str]] = None, ) -> None: + """ + #### Parameters + names - + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ assert isinstance(contract_name, Constant) assert is_valid_lvalue(lvalue) - super().__init__() + super().__init__(names=names) self._contract_name = contract_name # todo create analyze to add the contract instance self._lvalue = lvalue diff --git a/slither/slithir/operations/new_structure.py b/slither/slithir/operations/new_structure.py index c50cd6a3e0..2aaba28ff7 100644 --- a/slither/slithir/operations/new_structure.py +++ b/slither/slithir/operations/new_structure.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import List, Optional, Union from slither.slithir.operations.call import Call from slither.slithir.operations.lvalue import OperationWithLValue @@ -17,8 +17,15 @@ def __init__( self, structure: StructureContract, lvalue: Union[TemporaryVariableSSA, TemporaryVariable], + names: Optional[List[str]] = None, ) -> None: - super().__init__() + """ + #### Parameters + names - + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ + super().__init__(names=names) assert isinstance(structure, Structure) assert is_valid_lvalue(lvalue) self._structure = structure diff --git a/slither/slithir/operations/unary.py b/slither/slithir/operations/unary.py index c6493921dc..72e7ac63ae 100644 --- a/slither/slithir/operations/unary.py +++ b/slither/slithir/operations/unary.py @@ -5,7 +5,6 @@ from slither.slithir.operations.lvalue import OperationWithLValue from slither.slithir.utils.utils import is_valid_lvalue, is_valid_rvalue from slither.slithir.exceptions import SlithIRError -from slither.core.expressions.unary_operation import UnaryOperationType from slither.core.variables.local_variable import LocalVariable from slither.slithir.variables.constant import Constant from slither.slithir.variables.local_variable import LocalIRVariable @@ -35,7 +34,7 @@ def __init__( self, result: Union[TemporaryVariableSSA, TemporaryVariable], variable: Union[Constant, LocalIRVariable, LocalVariable], - operation_type: UnaryOperationType, + operation_type: UnaryType, ) -> None: assert is_valid_rvalue(variable) assert is_valid_lvalue(result) @@ -53,7 +52,7 @@ def rvalue(self) -> Union[Constant, LocalVariable]: return self._variable @property - def type(self) -> UnaryOperationType: + def type(self) -> UnaryType: return self._type @property diff --git a/slither/slithir/tmp_operations/tmp_call.py b/slither/slithir/tmp_operations/tmp_call.py index 2137ebd819..a7bd614c74 100644 --- a/slither/slithir/tmp_operations/tmp_call.py +++ b/slither/slithir/tmp_operations/tmp_call.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import List, Optional, Union from slither.core.declarations import ( Event, @@ -25,7 +25,15 @@ def __init__( nbr_arguments: int, result: Union[TupleVariable, TemporaryVariable], type_call: str, + names: Optional[List[str]] = None, ) -> None: + # pylint: disable=too-many-arguments + """ + #### Parameters + names - + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ assert isinstance( called, ( @@ -42,6 +50,7 @@ def __init__( self._called = called self._nbr_arguments = nbr_arguments self._type_call = type_call + self._names = names self._lvalue = result self._ori = None # self._callid = None @@ -49,6 +58,14 @@ def __init__( self._value = None self._salt = None + @property + def names(self) -> Optional[List[str]]: + """ + For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order. + Otherwise, None. + """ + return self._names + @property def call_value(self): return self._value diff --git a/slither/slithir/utils/ssa.py b/slither/slithir/utils/ssa.py index 4c958798b4..ef908820eb 100644 --- a/slither/slithir/utils/ssa.py +++ b/slither/slithir/utils/ssa.py @@ -735,12 +735,17 @@ def copy_ir(ir: Operation, *instances) -> Operation: destination = get_variable(ir, lambda x: x.destination, *instances) function_name = ir.function_name nbr_arguments = ir.nbr_arguments + names = ir.names lvalue = get_variable(ir, lambda x: x.lvalue, *instances) type_call = ir.type_call if isinstance(ir, LibraryCall): - new_ir = LibraryCall(destination, function_name, nbr_arguments, lvalue, type_call) + new_ir = LibraryCall( + destination, function_name, nbr_arguments, lvalue, type_call, names=names + ) else: - new_ir = HighLevelCall(destination, function_name, nbr_arguments, lvalue, type_call) + new_ir = HighLevelCall( + destination, function_name, nbr_arguments, lvalue, type_call, names=names + ) new_ir.call_id = ir.call_id new_ir.call_value = get_variable(ir, lambda x: x.call_value, *instances) new_ir.call_gas = get_variable(ir, lambda x: x.call_gas, *instances) @@ -761,7 +766,8 @@ def copy_ir(ir: Operation, *instances) -> Operation: nbr_arguments = ir.nbr_arguments lvalue = get_variable(ir, lambda x: x.lvalue, *instances) type_call = ir.type_call - new_ir = InternalCall(function, nbr_arguments, lvalue, type_call) + names = ir.names + new_ir = InternalCall(function, nbr_arguments, lvalue, type_call, names=names) new_ir.arguments = get_arguments(ir, *instances) return new_ir if isinstance(ir, InternalDynamicCall): @@ -811,7 +817,8 @@ def copy_ir(ir: Operation, *instances) -> Operation: if isinstance(ir, NewStructure): structure = ir.structure lvalue = get_variable(ir, lambda x: x.lvalue, *instances) - new_ir = NewStructure(structure, lvalue) + names = ir.names + new_ir = NewStructure(structure, lvalue, names=names) new_ir.arguments = get_arguments(ir, *instances) return new_ir if isinstance(ir, Nop): diff --git a/slither/solc_parsing/declarations/contract.py b/slither/solc_parsing/declarations/contract.py index 74a1cbcf0d..e7d2745c69 100644 --- a/slither/solc_parsing/declarations/contract.py +++ b/slither/solc_parsing/declarations/contract.py @@ -291,10 +291,10 @@ def _parse_type_alias(self, item: Dict) -> None: alias = item["name"] alias_canonical = self._contract.name + "." + item["name"] - user_defined_type = TypeAliasContract(original_type, alias, self.underlying_contract) - user_defined_type.set_offset(item["src"], self.compilation_unit) - self._contract.file_scope.user_defined_types[alias] = user_defined_type - self._contract.file_scope.user_defined_types[alias_canonical] = user_defined_type + type_alias = TypeAliasContract(original_type, alias, self.underlying_contract) + type_alias.set_offset(item["src"], self.compilation_unit) + self._contract.type_aliases_as_dict[alias] = type_alias + self._contract.file_scope.type_aliases[alias_canonical] = type_alias def _parse_struct(self, struct: Dict) -> None: @@ -319,7 +319,7 @@ def _parse_custom_error(self, custom_error: Dict) -> None: ce.set_contract(self._contract) ce.set_offset(custom_error["src"], self.compilation_unit) - ce_parser = CustomErrorSolc(ce, custom_error, self._slither_parser) + ce_parser = CustomErrorSolc(ce, custom_error, self, self._slither_parser) self._contract.custom_errors_as_dict[ce.name] = ce self._custom_errors_parser.append(ce_parser) diff --git a/slither/solc_parsing/declarations/custom_error.py b/slither/solc_parsing/declarations/custom_error.py index 8cd4592623..34b7ca402f 100644 --- a/slither/solc_parsing/declarations/custom_error.py +++ b/slither/solc_parsing/declarations/custom_error.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Dict, Optional from slither.core.declarations.custom_error import CustomError from slither.core.declarations.custom_error_contract import CustomErrorContract @@ -10,6 +10,7 @@ if TYPE_CHECKING: from slither.solc_parsing.slither_compilation_unit_solc import SlitherCompilationUnitSolc from slither.core.compilation_unit import SlitherCompilationUnit + from slither.solc_parsing.declarations.contract import ContractSolc # Part of the code was copied from the function parsing @@ -21,11 +22,13 @@ def __init__( self, custom_error: CustomError, custom_error_data: dict, + contract_parser: Optional["ContractSolc"], slither_parser: "SlitherCompilationUnitSolc", ) -> None: self._slither_parser: "SlitherCompilationUnitSolc" = slither_parser self._custom_error = custom_error custom_error.name = custom_error_data["name"] + self._contract_parser = contract_parser self._params_was_analyzed = False if not self._slither_parser.is_compact_ast: @@ -56,6 +59,10 @@ def analyze_params(self) -> None: if params: self._parse_params(params) + @property + def contract_parser(self) -> Optional["ContractSolc"]: + return self._contract_parser + @property def is_compact_ast(self) -> bool: return self._slither_parser.is_compact_ast diff --git a/slither/solc_parsing/declarations/using_for_top_level.py b/slither/solc_parsing/declarations/using_for_top_level.py index fe72e57809..bfed3c417c 100644 --- a/slither/solc_parsing/declarations/using_for_top_level.py +++ b/slither/solc_parsing/declarations/using_for_top_level.py @@ -152,7 +152,7 @@ def _propagate_global(self, type_name: Union[TypeAliasTopLevel, UserDefinedType] if self._global: for scope in self.compilation_unit.scopes.values(): if isinstance(type_name, TypeAliasTopLevel): - for alias in scope.user_defined_types.values(): + for alias in scope.type_aliases.values(): if alias == type_name: scope.using_for_directives.add(self._using_for) elif isinstance(type_name, UserDefinedType): diff --git a/slither/solc_parsing/expressions/expression_parsing.py b/slither/solc_parsing/expressions/expression_parsing.py index a0bce044c2..4ed896ed9c 100644 --- a/slither/solc_parsing/expressions/expression_parsing.py +++ b/slither/solc_parsing/expressions/expression_parsing.py @@ -175,11 +175,12 @@ def parse_call( called = parse_expression(children[0], caller_context) arguments = [parse_expression(a, caller_context) for a in children[1::]] - if isinstance(called, SuperCallExpression): + if isinstance(called, SuperIdentifier): sp = SuperCallExpression(called, arguments, type_return) sp.set_offset(expression["src"], caller_context.compilation_unit) return sp - call_expression = CallExpression(called, arguments, type_return) + names = expression["names"] if "names" in expression and len(expression["names"]) > 0 else None + call_expression = CallExpression(called, arguments, type_return, names=names) call_expression.set_offset(src, caller_context.compilation_unit) # Only available if the syntax {gas:, value:} was used diff --git a/slither/solc_parsing/expressions/find_variable.py b/slither/solc_parsing/expressions/find_variable.py index 32f5afc584..0a1275f411 100644 --- a/slither/solc_parsing/expressions/find_variable.py +++ b/slither/solc_parsing/expressions/find_variable.py @@ -3,6 +3,8 @@ from slither.core.declarations import Event, Enum, Structure from slither.core.declarations.contract import Contract from slither.core.declarations.custom_error import CustomError +from slither.core.declarations.custom_error_contract import CustomErrorContract +from slither.core.declarations.custom_error_top_level import CustomErrorTopLevel from slither.core.declarations.function import Function from slither.core.declarations.function_contract import FunctionContract from slither.core.declarations.function_top_level import FunctionTopLevel @@ -114,6 +116,8 @@ def find_top_level( :return: :rtype: """ + if var_name in scope.type_aliases: + return scope.type_aliases[var_name], False if var_name in scope.structures: return scope.structures[var_name], False @@ -205,6 +209,10 @@ def _find_in_contract( if sig == var_name: return modifier + type_aliases = contract.type_aliases_as_dict + if var_name in type_aliases: + return type_aliases[var_name] + # structures are looked on the contract declarer structures = contract.structures_as_dict if var_name in structures: @@ -240,6 +248,7 @@ def _find_in_contract( return None +# pylint: disable=too-many-statements def _find_variable_init( caller_context: CallerContextExpression, ) -> Tuple[List[Contract], List["Function"], FileScope,]: @@ -247,6 +256,7 @@ def _find_variable_init( from slither.solc_parsing.declarations.function import FunctionSolc from slither.solc_parsing.declarations.structure_top_level import StructureTopLevelSolc from slither.solc_parsing.variables.top_level_variable import TopLevelVariableSolc + from slither.solc_parsing.declarations.custom_error import CustomErrorSolc direct_contracts: List[Contract] direct_functions_parser: List[Function] @@ -289,6 +299,24 @@ def _find_variable_init( direct_contracts = [] direct_functions_parser = [] scope = caller_context.underlying_variable.file_scope + elif isinstance(caller_context, CustomErrorSolc): + if caller_context.contract_parser: + direct_contracts = [caller_context.contract_parser.underlying_contract] + direct_functions_parser = [ + f.underlying_function + for f in caller_context.contract_parser.functions_parser + + caller_context.contract_parser.modifiers_parser + ] + else: + # Top level custom error + direct_contracts = [] + direct_functions_parser = [] + underlying_custom_error = caller_context.underlying_custom_error + if isinstance(underlying_custom_error, CustomErrorTopLevel): + scope = underlying_custom_error.file_scope + else: + assert isinstance(underlying_custom_error, CustomErrorContract) + scope = underlying_custom_error.contract.file_scope else: raise SlitherError( f"{type(caller_context)} ({caller_context} is not valid for find_variable" @@ -337,6 +365,7 @@ def find_variable( """ from slither.solc_parsing.declarations.function import FunctionSolc from slither.solc_parsing.declarations.contract import ContractSolc + from slither.solc_parsing.declarations.custom_error import CustomErrorSolc # variable are looked from the contract declarer # functions can be shadowed, but are looked from the contract instance, rather than the contract declarer @@ -362,9 +391,6 @@ def find_variable( if var_name in current_scope.renaming: var_name = current_scope.renaming[var_name] - if var_name in current_scope.user_defined_types: - return current_scope.user_defined_types[var_name], False - # Use ret0/ret1 to help mypy ret0 = _find_variable_from_ref_declaration( referenced_declaration, direct_contracts, direct_functions @@ -391,6 +417,15 @@ def find_variable( contract_declarer = underlying_func.contract_declarer else: assert isinstance(underlying_func, FunctionTopLevel) + elif isinstance(caller_context, CustomErrorSolc): + underlying_custom_error = caller_context.underlying_custom_error + if isinstance(underlying_custom_error, CustomErrorContract): + contract = underlying_custom_error.contract + # We check for contract variables here because _find_in_contract + # will return since in this case the contract_declarer is None + for var in contract.variables: + if var_name == var.name: + return var, False ret = _find_in_contract(var_name, contract, contract_declarer, is_super, is_identifier_path) if ret: diff --git a/slither/solc_parsing/slither_compilation_unit_solc.py b/slither/solc_parsing/slither_compilation_unit_solc.py index cc61deb1f5..eb10171224 100644 --- a/slither/solc_parsing/slither_compilation_unit_solc.py +++ b/slither/solc_parsing/slither_compilation_unit_solc.py @@ -324,7 +324,7 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: custom_error = CustomErrorTopLevel(self._compilation_unit, scope) custom_error.set_offset(top_level_data["src"], self._compilation_unit) - custom_error_parser = CustomErrorSolc(custom_error, top_level_data, self) + custom_error_parser = CustomErrorSolc(custom_error, top_level_data, None, self) scope.custom_errors.add(custom_error) self._compilation_unit.custom_errors.append(custom_error) self._custom_error_parser.append(custom_error_parser) @@ -342,10 +342,10 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: original_type = ElementaryType(underlying_type["name"]) - user_defined_type = TypeAliasTopLevel(original_type, alias, scope) - user_defined_type.set_offset(top_level_data["src"], self._compilation_unit) - self._compilation_unit.user_defined_value_types[alias] = user_defined_type - scope.user_defined_types[alias] = user_defined_type + type_alias = TypeAliasTopLevel(original_type, alias, scope) + type_alias.set_offset(top_level_data["src"], self._compilation_unit) + self._compilation_unit.type_aliases[alias] = type_alias + scope.type_aliases[alias] = type_alias else: raise SlitherException(f"Top level {top_level_data[self.get_key()]} not supported") diff --git a/slither/solc_parsing/solidity_types/type_parsing.py b/slither/solc_parsing/solidity_types/type_parsing.py index e12290722f..c03b8e6562 100644 --- a/slither/solc_parsing/solidity_types/type_parsing.py +++ b/slither/solc_parsing/solidity_types/type_parsing.py @@ -235,7 +235,7 @@ def parse_type( sl: "SlitherCompilationUnit" renaming: Dict[str, str] - user_defined_types: Dict[str, TypeAlias] + type_aliases: Dict[str, TypeAlias] enums_direct_access: List["Enum"] = [] # Note: for convenicence top level functions use the same parser than function in contract # but contract_parser is set to None @@ -247,13 +247,13 @@ def parse_type( sl = caller_context.compilation_unit next_context = caller_context renaming = {} - user_defined_types = sl.user_defined_value_types + type_aliases = sl.type_aliases else: assert isinstance(caller_context, FunctionSolc) sl = caller_context.underlying_function.compilation_unit next_context = caller_context.slither_parser renaming = caller_context.underlying_function.file_scope.renaming - user_defined_types = caller_context.underlying_function.file_scope.user_defined_types + type_aliases = caller_context.underlying_function.file_scope.type_aliases structures_direct_access = sl.structures_top_level all_structuress = [c.structures for c in sl.contracts] all_structures = [item for sublist in all_structuress for item in sublist] @@ -299,7 +299,7 @@ def parse_type( functions = list(scope.functions) renaming = scope.renaming - user_defined_types = scope.user_defined_types + type_aliases = scope.type_aliases elif isinstance(caller_context, (ContractSolc, FunctionSolc)): sl = caller_context.compilation_unit if isinstance(caller_context, FunctionSolc): @@ -329,7 +329,7 @@ def parse_type( functions = contract.functions + contract.modifiers renaming = scope.renaming - user_defined_types = scope.user_defined_types + type_aliases = scope.type_aliases else: raise ParsingError(f"Incorrect caller context: {type(caller_context)}") @@ -343,8 +343,8 @@ def parse_type( name = t.name if name in renaming: name = renaming[name] - if name in user_defined_types: - return user_defined_types[name] + if name in type_aliases: + return type_aliases[name] return _find_from_type_name( name, functions, @@ -365,9 +365,9 @@ def parse_type( name = t["typeDescriptions"]["typeString"] if name in renaming: name = renaming[name] - if name in user_defined_types: - _add_type_references(user_defined_types[name], t["src"], sl) - return user_defined_types[name] + if name in type_aliases: + _add_type_references(type_aliases[name], t["src"], sl) + return type_aliases[name] type_found = _find_from_type_name( name, functions, @@ -386,9 +386,9 @@ def parse_type( name = t["attributes"][type_name_key] if name in renaming: name = renaming[name] - if name in user_defined_types: - _add_type_references(user_defined_types[name], t["src"], sl) - return user_defined_types[name] + if name in type_aliases: + _add_type_references(type_aliases[name], t["src"], sl) + return type_aliases[name] type_found = _find_from_type_name( name, functions, @@ -407,8 +407,8 @@ def parse_type( name = t["name"] if name in renaming: name = renaming[name] - if name in user_defined_types: - return user_defined_types[name] + if name in type_aliases: + return type_aliases[name] type_found = _find_from_type_name( name, functions, diff --git a/slither/utils/ck.py b/slither/utils/ck.py new file mode 100644 index 0000000000..ffba663ad2 --- /dev/null +++ b/slither/utils/ck.py @@ -0,0 +1,348 @@ +""" + CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. + These metrics are used to measure the complexity of a class. + https://en.wikipedia.org/wiki/Programming_complexity + + - Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. + - Number of Children (NOC) is a metric that measures the number of children a class has. + - Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. + - Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. + + Not implemented: + - Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. + - Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. + + During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated. + These are also included in the output: + - State variables: total number of state variables + - Constants: total number of constants + - Immutables: total number of immutables + - Public: total number of public functions + - External: total number of external functions + - Internal: total number of internal functions + - Private: total number of private functions + - Mutating: total number of state mutating functions + - View: total number of view functions + - Pure: total number of pure functions + - External mutating: total number of external mutating functions + - No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers + - No modifiers: total number of functions without modifiers + - Ext calls: total number of external calls + +""" +from collections import OrderedDict +from typing import Tuple, List, Dict +from dataclasses import dataclass, field +from slither.utils.colors import bold +from slither.core.declarations import Contract +from slither.utils.myprettytable import make_pretty_table, MyPrettyTable +from slither.utils.martin import MartinMetrics +from slither.slithir.operations.high_level_call import HighLevelCall + + +# Utility functions + + +def compute_dit(contract: Contract, depth: int = 0) -> int: + """ + Recursively compute the depth of inheritance tree (DIT) of a contract + Args: + contract(core.declarations.contract.Contract): contract to compute DIT for + depth(int): current depth of the contract + Returns: + int: depth of the contract + """ + if not contract.inheritance: + return depth + max_dit = depth + for inherited_contract in contract.inheritance: + dit = compute_dit(inherited_contract, depth + 1) + max_dit = max(max_dit, dit) + return max_dit + + +def has_auth(func) -> bool: + """ + Check if a function has no auth or only_owner modifiers + Args: + func(core.declarations.function.Function): function to check + Returns: + bool True if it does have auth or only_owner modifiers + """ + for modifier in func.modifiers: + if "auth" in modifier.name or "only_owner" in modifier.name: + return True + return False + + +# Utility classes for calculating CK metrics + + +@dataclass +# pylint: disable=too-many-instance-attributes +class CKContractMetrics: + """Class to hold the CK metrics for a single contract.""" + + contract: Contract + + # Used to calculate CBO - should be passed in as a constructor arg + martin_metrics: Dict + + # Used to calculate NOC + dependents: Dict + + state_variables: int = 0 + constants: int = 0 + immutables: int = 0 + public: int = 0 + external: int = 0 + internal: int = 0 + private: int = 0 + mutating: int = 0 + view: int = 0 + pure: int = 0 + external_mutating: int = 0 + no_auth_or_only_owner: int = 0 + no_modifiers: int = 0 + ext_calls: int = 0 + rfc: int = 0 + noc: int = 0 + dit: int = 0 + cbo: int = 0 + + def __post_init__(self) -> None: + if not hasattr(self.contract, "functions"): + return + self.count_variables() + self.noc = len(self.dependents[self.contract.name]) + self.dit = compute_dit(self.contract) + self.cbo = ( + self.martin_metrics[self.contract.name].ca + self.martin_metrics[self.contract.name].ce + ) + self.calculate_metrics() + + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + def calculate_metrics(self) -> None: + """Calculate the metrics for a contract""" + rfc = self.public # initialize with public getter count + for func in self.contract.functions: + if func.name == "constructor": + continue + pure = func.pure + view = not pure and func.view + mutating = not pure and not view + external = func.visibility == "external" + public = func.visibility == "public" + internal = func.visibility == "internal" + private = func.visibility == "private" + external_public_mutating = external or public and mutating + external_no_auth = external_public_mutating and not has_auth(func) + external_no_modifiers = external_public_mutating and len(func.modifiers) == 0 + if external or public: + rfc += 1 + + high_level_calls = [ + ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) + ] + + # convert irs to string with target function and contract name + external_calls = [] + for high_level_call in high_level_calls: + if isinstance(high_level_call.destination, Contract): + destination_contract = high_level_call.destination.name + elif isinstance(high_level_call.destination, str): + destination_contract = high_level_call.destination + elif not hasattr(high_level_call.destination, "type"): + continue + elif isinstance(high_level_call.destination.type, Contract): + destination_contract = high_level_call.destination.type.name + elif isinstance(high_level_call.destination.type, str): + destination_contract = high_level_call.destination.type + elif not hasattr(high_level_call.destination.type, "type"): + continue + elif isinstance(high_level_call.destination.type.type, Contract): + destination_contract = high_level_call.destination.type.type.name + elif isinstance(high_level_call.destination.type.type, str): + destination_contract = high_level_call.destination.type.type + else: + continue + external_calls.append(f"{high_level_call.function_name}{destination_contract}") + rfc += len(set(external_calls)) + + self.public += public + self.external += external + self.internal += internal + self.private += private + + self.mutating += mutating + self.view += view + self.pure += pure + + self.external_mutating += external_public_mutating + self.no_auth_or_only_owner += external_no_auth + self.no_modifiers += external_no_modifiers + + self.ext_calls += len(external_calls) + self.rfc = rfc + + def count_variables(self) -> None: + """Count the number of variables in a contract""" + state_variable_count = 0 + constant_count = 0 + immutable_count = 0 + public_getter_count = 0 + for variable in self.contract.variables: + if variable.is_constant: + constant_count += 1 + elif variable.is_immutable: + immutable_count += 1 + else: + state_variable_count += 1 + if variable.visibility == "Public": + public_getter_count += 1 + self.state_variables = state_variable_count + self.constants = constant_count + self.immutables = immutable_count + + # initialize RFC with public getter count + # self.public is used count public functions not public variables + self.rfc = public_getter_count + + def to_dict(self) -> Dict[str, float]: + """Return the metrics as a dictionary.""" + return OrderedDict( + { + "State variables": self.state_variables, + "Constants": self.constants, + "Immutables": self.immutables, + "Public": self.public, + "External": self.external, + "Internal": self.internal, + "Private": self.private, + "Mutating": self.mutating, + "View": self.view, + "Pure": self.pure, + "External mutating": self.external_mutating, + "No auth or onlyOwner": self.no_auth_or_only_owner, + "No modifiers": self.no_modifiers, + "Ext calls": self.ext_calls, + "RFC": self.rfc, + "NOC": self.noc, + "DIT": self.dit, + "CBO": self.cbo, + } + ) + + +@dataclass +class SectionInfo: + """Class to hold the information for a section of the report.""" + + title: str + pretty_table: MyPrettyTable + txt: str + + +@dataclass +# pylint: disable=too-many-instance-attributes +class CKMetrics: + """Class to hold the CK metrics for all contracts. Contains methods useful for reporting. + + There are 5 sections in the report: + 1. Variable count by type (state, constant, immutable) + 2. Function count by visibility (public, external, internal, private) + 3. Function count by mutability (mutating, view, pure) + 4. External mutating function count by modifier (external mutating, no auth or onlyOwner, no modifiers) + 5. CK metrics (RFC, NOC, DIT, CBO) + """ + + contracts: List[Contract] = field(default_factory=list) + contract_metrics: OrderedDict = field(default_factory=OrderedDict) + title: str = "CK complexity metrics" + full_text: str = "" + auxiliary1: SectionInfo = field(default=SectionInfo) + auxiliary2: SectionInfo = field(default=SectionInfo) + auxiliary3: SectionInfo = field(default=SectionInfo) + auxiliary4: SectionInfo = field(default=SectionInfo) + core: SectionInfo = field(default=SectionInfo) + AUXILIARY1_KEYS = ( + "State variables", + "Constants", + "Immutables", + ) + AUXILIARY2_KEYS = ( + "Public", + "External", + "Internal", + "Private", + ) + AUXILIARY3_KEYS = ( + "Mutating", + "View", + "Pure", + ) + AUXILIARY4_KEYS = ( + "External mutating", + "No auth or onlyOwner", + "No modifiers", + ) + CORE_KEYS = ( + "Ext calls", + "RFC", + "NOC", + "DIT", + "CBO", + ) + SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = ( + ("Variables", "auxiliary1", AUXILIARY1_KEYS), + ("Function visibility", "auxiliary2", AUXILIARY2_KEYS), + ("State mutability", "auxiliary3", AUXILIARY3_KEYS), + ("External mutating functions", "auxiliary4", AUXILIARY4_KEYS), + ("Core", "core", CORE_KEYS), + ) + + def __post_init__(self) -> None: + martin_metrics = MartinMetrics(self.contracts).contract_metrics + dependents = { + inherited.name: { + contract.name + for contract in self.contracts + if inherited.name in contract.inheritance + } + for inherited in self.contracts + } + for contract in self.contracts: + self.contract_metrics[contract.name] = CKContractMetrics( + contract=contract, martin_metrics=martin_metrics, dependents=dependents + ) + + # Create the table and text for each section. + data = { + contract.name: self.contract_metrics[contract.name].to_dict() + for contract in self.contracts + } + + subtitle = "" + # Update each section + for (title, attr, keys) in self.SECTIONS: + if attr == "core": + # Special handling for core section + totals_enabled = False + subtitle += bold("RFC: Response For a Class\n") + subtitle += bold("NOC: Number of Children\n") + subtitle += bold("DIT: Depth of Inheritance Tree\n") + subtitle += bold("CBO: Coupling Between Object Classes\n") + else: + totals_enabled = True + subtitle = "" + + pretty_table = make_pretty_table(["Contract", *keys], data, totals=totals_enabled) + section_title = f"{self.title} ({title})" + txt = f"\n\n{section_title}:\n{subtitle}{pretty_table}\n" + self.full_text += txt + setattr( + self, + attr, + SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), + ) diff --git a/slither/utils/encoding.py b/slither/utils/encoding.py new file mode 100644 index 0000000000..288b581505 --- /dev/null +++ b/slither/utils/encoding.py @@ -0,0 +1,202 @@ +from typing import Union + +from slither.core import variables +from slither.core.declarations import ( + SolidityVariable, + SolidityVariableComposed, + Structure, + Enum, + Contract, +) +from slither.core import solidity_types +from slither.slithir import operations +from slither.slithir import variables as SlitherIRVariable + + +# pylint: disable=too-many-branches +def ntype(_type: Union[solidity_types.Type, str]) -> str: + if isinstance(_type, solidity_types.ElementaryType): + _type = str(_type) + elif isinstance(_type, solidity_types.ArrayType): + if isinstance(_type.type, solidity_types.ElementaryType): + _type = str(_type) + else: + _type = "user_defined_array" + elif isinstance(_type, Structure): + _type = str(_type) + elif isinstance(_type, Enum): + _type = str(_type) + elif isinstance(_type, solidity_types.MappingType): + _type = str(_type) + elif isinstance(_type, solidity_types.UserDefinedType): + if isinstance(_type.type, Contract): + _type = f"contract({_type.type.name})" + elif isinstance(_type.type, Structure): + _type = f"struct({_type.type.name})" + elif isinstance(_type.type, Enum): + _type = f"enum({_type.type.name})" + else: + _type = str(_type) + + _type = _type.replace(" memory", "") + _type = _type.replace(" storage ref", "") + + if "struct" in _type: + return "struct" + if "enum" in _type: + return "enum" + if "tuple" in _type: + return "tuple" + if "contract" in _type: + return "contract" + if "mapping" in _type: + return "mapping" + return _type.replace(" ", "_") + + +# pylint: disable=too-many-branches +def encode_var_for_compare(var: Union[variables.Variable, SolidityVariable]) -> str: + + # variables + if isinstance(var, SlitherIRVariable.Constant): + return f"constant({ntype(var.type)},{var.value})" + if isinstance(var, SolidityVariableComposed): + return f"solidity_variable_composed({var.name})" + if isinstance(var, SolidityVariable): + return f"solidity_variable{var.name}" + if isinstance(var, SlitherIRVariable.TemporaryVariable): + return "temporary_variable" + if isinstance(var, SlitherIRVariable.ReferenceVariable): + return f"reference({ntype(var.type)})" + if isinstance(var, variables.LocalVariable): + return f"local_solc_variable({ntype(var.type)},{var.location})" + if isinstance(var, variables.StateVariable): + if not (var.is_constant or var.is_immutable): + try: + slot, _ = var.contract.compilation_unit.storage_layout_of(var.contract, var) + except KeyError: + slot = var.name + else: + slot = var.name + return f"state_solc_variable({ntype(var.type)},{slot})" + if isinstance(var, variables.LocalVariableInitFromTuple): + return "local_variable_init_tuple" + if isinstance(var, SlitherIRVariable.TupleVariable): + return "tuple_variable" + + # default + return "" + + +# pylint: disable=too-many-branches +def encode_ir_for_upgradeability_compare(ir: operations.Operation) -> str: + # operations + if isinstance(ir, operations.Assignment): + return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})" + if isinstance(ir, operations.Index): + return f"index({ntype(ir.variable_right.type)})" + if isinstance(ir, operations.Member): + return "member" # .format(ntype(ir._type)) + if isinstance(ir, operations.Length): + return "length" + if isinstance(ir, operations.Binary): + return f"binary({encode_var_for_compare(ir.variable_left)}{ir.type}{encode_var_for_compare(ir.variable_right)})" + if isinstance(ir, operations.Unary): + return f"unary({str(ir.type)})" + if isinstance(ir, operations.Condition): + return f"condition({encode_var_for_compare(ir.value)})" + if isinstance(ir, operations.NewStructure): + return "new_structure" + if isinstance(ir, operations.NewContract): + return "new_contract" + if isinstance(ir, operations.NewArray): + return f"new_array({ntype(ir.array_type)})" + if isinstance(ir, operations.NewElementaryType): + return f"new_elementary({ntype(ir.type)})" + if isinstance(ir, operations.Delete): + return f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})" + if isinstance(ir, operations.SolidityCall): + return f"solidity_call({ir.function.full_name})" + if isinstance(ir, operations.InternalCall): + return f"internal_call({ntype(ir.type_call)})" + if isinstance(ir, operations.EventCall): # is this useful? + return "event" + if isinstance(ir, operations.LibraryCall): + return "library_call" + if isinstance(ir, operations.InternalDynamicCall): + return "internal_dynamic_call" + if isinstance(ir, operations.HighLevelCall): # TODO: improve + return "high_level_call" + if isinstance(ir, operations.LowLevelCall): # TODO: improve + return "low_level_call" + if isinstance(ir, operations.TypeConversion): + return f"type_conversion({ntype(ir.type)})" + if isinstance(ir, operations.Return): # this can be improved using values + return "return" # .format(ntype(ir.type)) + if isinstance(ir, operations.Transfer): + return f"transfer({encode_var_for_compare(ir.call_value)})" + if isinstance(ir, operations.Send): + return f"send({encode_var_for_compare(ir.call_value)})" + if isinstance(ir, operations.Unpack): # TODO: improve + return "unpack" + if isinstance(ir, operations.InitArray): # TODO: improve + return "init_array" + + # default + return "" + + +def encode_ir_for_halstead(ir: operations.Operation) -> str: + # operations + if isinstance(ir, operations.Assignment): + return "assignment" + if isinstance(ir, operations.Index): + return "index" + if isinstance(ir, operations.Member): + return "member" # .format(ntype(ir._type)) + if isinstance(ir, operations.Length): + return "length" + if isinstance(ir, operations.Binary): + return f"binary({str(ir.type)})" + if isinstance(ir, operations.Unary): + return f"unary({str(ir.type)})" + if isinstance(ir, operations.Condition): + return f"condition({encode_var_for_compare(ir.value)})" + if isinstance(ir, operations.NewStructure): + return "new_structure" + if isinstance(ir, operations.NewContract): + return "new_contract" + if isinstance(ir, operations.NewArray): + return f"new_array({ntype(ir.array_type)})" + if isinstance(ir, operations.NewElementaryType): + return f"new_elementary({ntype(ir.type)})" + if isinstance(ir, operations.Delete): + return "delete" + if isinstance(ir, operations.SolidityCall): + return f"solidity_call({ir.function.full_name})" + if isinstance(ir, operations.InternalCall): + return f"internal_call({ntype(ir.type_call)})" + if isinstance(ir, operations.EventCall): # is this useful? + return "event" + if isinstance(ir, operations.LibraryCall): + return "library_call" + if isinstance(ir, operations.InternalDynamicCall): + return "internal_dynamic_call" + if isinstance(ir, operations.HighLevelCall): # TODO: improve + return "high_level_call" + if isinstance(ir, operations.LowLevelCall): # TODO: improve + return "low_level_call" + if isinstance(ir, operations.TypeConversion): + return f"type_conversion({ntype(ir.type)})" + if isinstance(ir, operations.Return): # this can be improved using values + return "return" # .format(ntype(ir.type)) + if isinstance(ir, operations.Transfer): + return "transfer" + if isinstance(ir, operations.Send): + return "send" + if isinstance(ir, operations.Unpack): # TODO: improve + return "unpack" + if isinstance(ir, operations.InitArray): # TODO: improve + return "init_array" + # default + raise NotImplementedError(f"encode_ir_for_halstead: {ir}") diff --git a/slither/utils/expression_manipulations.py b/slither/utils/expression_manipulations.py index 75d97042c2..32b88e9b32 100644 --- a/slither/utils/expression_manipulations.py +++ b/slither/utils/expression_manipulations.py @@ -147,7 +147,7 @@ def convert_expressions( for next_expr in expression.expressions: # TODO: can we get rid of `NoneType` expressions in `TupleExpression`? # montyly: this might happen with unnamed tuple (ex: (,,,) = f()), but it needs to be checked - if next_expr: + if next_expr is not None: if self.conditional_not_ahead( next_expr, true_expression, false_expression, f_expressions @@ -158,6 +158,9 @@ def convert_expressions( true_expression.expressions[-1], false_expression.expressions[-1], ) + else: + true_expression.expressions.append(None) + false_expression.expressions.append(None) def convert_index_access( self, next_expr: IndexAccess, true_expression: Expression, false_expression: Expression diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py new file mode 100644 index 0000000000..9ec952e486 --- /dev/null +++ b/slither/utils/halstead.py @@ -0,0 +1,233 @@ +""" + Halstead complexity metrics + https://en.wikipedia.org/wiki/Halstead_complexity_measures + + 12 metrics based on the number of unique operators and operands: + + Core metrics: + n1 = the number of distinct operators + n2 = the number of distinct operands + N1 = the total number of operators + N2 = the total number of operands + + Extended metrics1: + n = n1 + n2 # Program vocabulary + N = N1 + N2 # Program length + S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length + V = N * log2(n) # Volume + + Extended metrics2: + D = (n1 / 2) * (N2 / n2) # Difficulty + E = D * V # Effort + T = E / 18 seconds # Time required to program + B = (E^(2/3)) / 3000 # Number of delivered bugs + + +""" +import math +from collections import OrderedDict +from dataclasses import dataclass, field +from typing import Tuple, List, Dict + +from slither.core.declarations import Contract +from slither.slithir.variables.temporary import TemporaryVariable +from slither.utils.encoding import encode_ir_for_halstead +from slither.utils.myprettytable import make_pretty_table, MyPrettyTable + + +# pylint: disable=too-many-branches + + +@dataclass +# pylint: disable=too-many-instance-attributes +class HalsteadContractMetrics: + """Class to hold the Halstead metrics for a single contract.""" + + contract: Contract + all_operators: List[str] = field(default_factory=list) + all_operands: List[str] = field(default_factory=list) + n1: int = 0 + n2: int = 0 + N1: int = 0 + N2: int = 0 + n: int = 0 + N: int = 0 + S: float = 0 + V: float = 0 + D: float = 0 + E: float = 0 + T: float = 0 + B: float = 0 + + def __post_init__(self) -> None: + """Operators and operands can be passed in as constructor args to avoid computing + them based on the contract. Useful for computing metrics for ALL_CONTRACTS""" + + if len(self.all_operators) == 0: + if not hasattr(self.contract, "functions"): + return + self.populate_operators_and_operands() + if len(self.all_operators) > 0: + self.compute_metrics() + + def to_dict(self) -> Dict[str, float]: + """Return the metrics as a dictionary.""" + return OrderedDict( + { + "Total Operators": self.N1, + "Unique Operators": self.n1, + "Total Operands": self.N2, + "Unique Operands": self.n2, + "Vocabulary": str(self.n1 + self.n2), + "Program Length": str(self.N1 + self.N2), + "Estimated Length": f"{self.S:.0f}", + "Volume": f"{self.V:.0f}", + "Difficulty": f"{self.D:.0f}", + "Effort": f"{self.E:.0f}", + "Time": f"{self.T:.0f}", + "Estimated Bugs": f"{self.B:.3f}", + } + ) + + def populate_operators_and_operands(self) -> None: + """Populate the operators and operands lists.""" + operators = [] + operands = [] + + for func in self.contract.functions: + for node in func.nodes: + for operation in node.irs: + # use operation.expression.type to get the unique operator type + encoded_operator = encode_ir_for_halstead(operation) + operators.append(encoded_operator) + + # use operation.used to get the operands of the operation ignoring the temporary variables + operands.extend( + [op for op in operation.used if not isinstance(op, TemporaryVariable)] + ) + self.all_operators.extend(operators) + self.all_operands.extend(operands) + + def compute_metrics(self, all_operators=None, all_operands=None) -> None: + """Compute the Halstead metrics.""" + if all_operators is None: + all_operators = self.all_operators + all_operands = self.all_operands + + # core metrics + self.n1 = len(set(all_operators)) + self.n2 = len(set(all_operands)) + self.N1 = len(all_operators) + self.N2 = len(all_operands) + if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]): + raise ValueError("n1 and n2 must be greater than 0") + + # extended metrics 1 + self.n = self.n1 + self.n2 + self.N = self.N1 + self.N2 + self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2) + self.V = self.N * math.log2(self.n) + + # extended metrics 2 + self.D = (self.n1 / 2) * (self.N2 / self.n2) + self.E = self.D * self.V + self.T = self.E / 18 + self.B = (self.E ** (2 / 3)) / 3000 + + +@dataclass +class SectionInfo: + """Class to hold the information for a section of the report.""" + + title: str + pretty_table: MyPrettyTable + txt: str + + +@dataclass +# pylint: disable=too-many-instance-attributes +class HalsteadMetrics: + """Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting. + + There are 3 sections in the report: + 1. Core metrics (n1, n2, N1, N2) + 2. Extended metrics 1 (n, N, S, V) + 3. Extended metrics 2 (D, E, T, B) + + """ + + contracts: List[Contract] = field(default_factory=list) + contract_metrics: OrderedDict = field(default_factory=OrderedDict) + title: str = "Halstead complexity metrics" + full_text: str = "" + core: SectionInfo = field(default=SectionInfo) + extended1: SectionInfo = field(default=SectionInfo) + extended2: SectionInfo = field(default=SectionInfo) + CORE_KEYS = ( + "Total Operators", + "Unique Operators", + "Total Operands", + "Unique Operands", + ) + EXTENDED1_KEYS = ( + "Vocabulary", + "Program Length", + "Estimated Length", + "Volume", + ) + EXTENDED2_KEYS = ( + "Difficulty", + "Effort", + "Time", + "Estimated Bugs", + ) + SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = ( + ("Core", "core", CORE_KEYS), + ("Extended 1/2", "extended1", EXTENDED1_KEYS), + ("Extended 2/2", "extended2", EXTENDED2_KEYS), + ) + + def __post_init__(self) -> None: + # Compute the metrics for each contract and for all contracts. + self.update_contract_metrics() + self.add_all_contracts_metrics() + self.update_reporting_sections() + + def update_contract_metrics(self) -> None: + for contract in self.contracts: + self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract) + + def add_all_contracts_metrics(self) -> None: + # If there are more than 1 contract, compute the metrics for all contracts. + if len(self.contracts) <= 1: + return + all_operators = [ + operator + for contract in self.contracts + for operator in self.contract_metrics[contract.name].all_operators + ] + all_operands = [ + operand + for contract in self.contracts + for operand in self.contract_metrics[contract.name].all_operands + ] + self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics( + None, all_operators=all_operators, all_operands=all_operands + ) + + def update_reporting_sections(self) -> None: + # Create the table and text for each section. + data = { + contract.name: self.contract_metrics[contract.name].to_dict() + for contract in self.contracts + } + for (title, attr, keys) in self.SECTIONS: + pretty_table = make_pretty_table(["Contract", *keys], data, False) + section_title = f"{self.title} ({title})" + txt = f"\n\n{section_title}:\n{pretty_table}\n" + self.full_text += txt + setattr( + self, + attr, + SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), + ) diff --git a/slither/utils/martin.py b/slither/utils/martin.py new file mode 100644 index 0000000000..c336227fd2 --- /dev/null +++ b/slither/utils/martin.py @@ -0,0 +1,157 @@ +""" + Robert "Uncle Bob" Martin - Agile software metrics + https://en.wikipedia.org/wiki/Software_package_metrics + + Efferent Coupling (Ce): Number of contracts that the contract depends on + Afferent Coupling (Ca): Number of contracts that depend on a contract + Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) + Abstractness (A): Number of abstract contracts / total number of contracts + Distance from the Main Sequence (D): abs(A + I - 1) + +""" +from typing import Tuple, List, Dict +from dataclasses import dataclass, field +from collections import OrderedDict +from slither.slithir.operations.high_level_call import HighLevelCall +from slither.core.declarations import Contract +from slither.utils.myprettytable import make_pretty_table, MyPrettyTable + + +@dataclass +class MartinContractMetrics: + contract: Contract + ca: int + ce: int + abstractness: float + i: float = 0.0 + d: float = 0.0 + + def __post_init__(self) -> None: + if self.ce + self.ca > 0: + self.i = float(self.ce / (self.ce + self.ca)) + self.d = float(abs(self.i - self.abstractness)) + + def to_dict(self) -> Dict: + return { + "Dependents": self.ca, + "Dependencies": self.ce, + "Instability": f"{self.i:.2f}", + "Distance from main sequence": f"{self.d:.2f}", + } + + +@dataclass +class SectionInfo: + """Class to hold the information for a section of the report.""" + + title: str + pretty_table: MyPrettyTable + txt: str + + +@dataclass +class MartinMetrics: + contracts: List[Contract] = field(default_factory=list) + abstractness: float = 0.0 + contract_metrics: OrderedDict = field(default_factory=OrderedDict) + title: str = "Martin complexity metrics" + full_text: str = "" + core: SectionInfo = field(default=SectionInfo) + CORE_KEYS = ( + "Dependents", + "Dependencies", + "Instability", + "Distance from main sequence", + ) + SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),) + + def __post_init__(self) -> None: + self.update_abstractness() + self.update_coupling() + self.update_reporting_sections() + + def update_reporting_sections(self) -> None: + # Create the table and text for each section. + data = { + contract.name: self.contract_metrics[contract.name].to_dict() + for contract in self.contracts + } + for (title, attr, keys) in self.SECTIONS: + pretty_table = make_pretty_table(["Contract", *keys], data, False) + section_title = f"{self.title} ({title})" + txt = f"\n\n{section_title}:\n" + txt = "Martin agile software metrics\n" + txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" + txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" + txt += ( + "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" + ) + txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" + txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" + txt += "\n" + txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" + txt += f"{pretty_table}\n" + self.full_text += txt + setattr( + self, + attr, + SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), + ) + + def update_abstractness(self) -> None: + abstract_contract_count = 0 + for c in self.contracts: + if not c.is_fully_implemented: + abstract_contract_count += 1 + self.abstractness = float(abstract_contract_count / len(self.contracts)) + + # pylint: disable=too-many-branches + def update_coupling(self) -> None: + dependencies = {} + for contract in self.contracts: + external_calls = [] + for func in contract.functions: + high_level_calls = [ + ir + for node in func.nodes + for ir in node.irs_ssa + if isinstance(ir, HighLevelCall) + ] + # convert irs to string with target function and contract name + # Get the target contract name for each high level call + new_external_calls = [] + for high_level_call in high_level_calls: + if isinstance(high_level_call.destination, Contract): + new_external_call = high_level_call.destination.name + elif isinstance(high_level_call.destination, str): + new_external_call = high_level_call.destination + elif not hasattr(high_level_call.destination, "type"): + continue + elif isinstance(high_level_call.destination.type, Contract): + new_external_call = high_level_call.destination.type.name + elif isinstance(high_level_call.destination.type, str): + new_external_call = high_level_call.destination.type + elif not hasattr(high_level_call.destination.type, "type"): + continue + elif isinstance(high_level_call.destination.type.type, Contract): + new_external_call = high_level_call.destination.type.type.name + elif isinstance(high_level_call.destination.type.type, str): + new_external_call = high_level_call.destination.type.type + else: + continue + new_external_calls.append(new_external_call) + external_calls.extend(new_external_calls) + dependencies[contract.name] = set(external_calls) + dependents = {} + for contract, deps in dependencies.items(): + for dep in deps: + if dep not in dependents: + dependents[dep] = set() + dependents[dep].add(contract) + + for contract in self.contracts: + ce = len(dependencies.get(contract.name, [])) + ca = len(dependents.get(contract.name, [])) + self.contract_metrics[contract.name] = MartinContractMetrics( + contract, ca, ce, self.abstractness + ) diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index 2f2be7e72d..d67f570c0a 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -4,9 +4,17 @@ class MyPrettyTable: - def __init__(self, field_names: List[str]): + def __init__(self, field_names: List[str], pretty_align: bool = True): # TODO: True by default? self._field_names = field_names self._rows: List = [] + self._options: Dict = {} + if pretty_align: + self._options["set_alignment"] = [] + self._options["set_alignment"] += [(field_names[0], "l")] + for field_name in field_names[1:]: + self._options["set_alignment"] += [(field_name, "r")] + else: + self._options["set_alignment"] = [] def add_row(self, row: List[Union[str, List[str]]]) -> None: self._rows.append(row) @@ -15,6 +23,9 @@ def to_pretty_table(self) -> ColorTable: table = ColorTable(self._field_names, theme=Themes.OCEAN) for row in self._rows: table.add_row(row) + if len(self._options["set_alignment"]): + for column_header, value in self._options["set_alignment"]: + table.align[column_header] = value return table def to_json(self) -> Dict: @@ -22,3 +33,30 @@ def to_json(self) -> Dict: def __str__(self) -> str: return str(self.to_pretty_table()) + + +# UTILITY FUNCTIONS + + +def make_pretty_table( + headers: list, body: dict, totals: bool = False, total_header="TOTAL" +) -> MyPrettyTable: + """ + Converts a dict to a MyPrettyTable. Dict keys are the row headers. + Args: + headers: str[] of column names + body: dict of row headers with a dict of the values + totals: bool optional add Totals row + total_header: str optional if totals is set to True this will override the default "TOTAL" header + Returns: + MyPrettyTable + """ + table = MyPrettyTable(headers) + for row in body: + table_row = [row] + [body[row][key] for key in headers[1:]] + table.add_row(table_row) + if totals: + table.add_row( + [total_header] + [sum([body[row][key] for row in body]) for key in headers[1:]] + ) + return table diff --git a/slither/utils/upgradeability.py b/slither/utils/upgradeability.py index 3bef3b9b61..bbb253175e 100644 --- a/slither/utils/upgradeability.py +++ b/slither/utils/upgradeability.py @@ -1,66 +1,28 @@ -from typing import Optional, Tuple, List, Union +from typing import Optional, Tuple, List + +from slither.analyses.data_dependency.data_dependency import get_dependencies +from slither.core.cfg.node import Node, NodeType from slither.core.declarations import ( Contract, - Structure, - Enum, - SolidityVariableComposed, - SolidityVariable, Function, ) -from slither.core.solidity_types import ( - Type, - ElementaryType, - ArrayType, - MappingType, - UserDefinedType, -) -from slither.core.variables.local_variable import LocalVariable -from slither.core.variables.local_variable_init_from_tuple import LocalVariableInitFromTuple -from slither.core.variables.state_variable import StateVariable -from slither.analyses.data_dependency.data_dependency import get_dependencies -from slither.core.variables.variable import Variable from slither.core.expressions import ( Literal, Identifier, CallExpression, AssignmentOperation, ) -from slither.core.cfg.node import Node, NodeType +from slither.core.solidity_types import ( + ElementaryType, +) +from slither.core.variables.local_variable import LocalVariable +from slither.core.variables.state_variable import StateVariable +from slither.core.variables.variable import Variable from slither.slithir.operations import ( - Operation, - Assignment, - Index, - Member, - Length, - Binary, - Unary, - Condition, - NewArray, - NewStructure, - NewContract, - NewElementaryType, - SolidityCall, - Delete, - EventCall, - LibraryCall, - InternalDynamicCall, - HighLevelCall, LowLevelCall, - TypeConversion, - Return, - Transfer, - Send, - Unpack, - InitArray, - InternalCall, -) -from slither.slithir.variables import ( - TemporaryVariable, - TupleVariable, - Constant, - ReferenceVariable, ) from slither.tools.read_storage.read_storage import SlotInfo, SlitherReadStorage +from slither.utils.encoding import encode_ir_for_upgradeability_compare class TaintedExternalContract: @@ -385,144 +347,13 @@ def is_function_modified(f1: Function, f2: Function) -> bool: if len(node_f1.irs) != len(node_f2.irs): return True for i, ir in enumerate(node_f1.irs): - if encode_ir_for_compare(ir) != encode_ir_for_compare(node_f2.irs[i]): + if encode_ir_for_upgradeability_compare(ir) != encode_ir_for_upgradeability_compare( + node_f2.irs[i] + ): return True return False -# pylint: disable=too-many-branches -def ntype(_type: Union[Type, str]) -> str: - if isinstance(_type, ElementaryType): - _type = str(_type) - elif isinstance(_type, ArrayType): - if isinstance(_type.type, ElementaryType): - _type = str(_type) - else: - _type = "user_defined_array" - elif isinstance(_type, Structure): - _type = str(_type) - elif isinstance(_type, Enum): - _type = str(_type) - elif isinstance(_type, MappingType): - _type = str(_type) - elif isinstance(_type, UserDefinedType): - if isinstance(_type.type, Contract): - _type = f"contract({_type.type.name})" - elif isinstance(_type.type, Structure): - _type = f"struct({_type.type.name})" - elif isinstance(_type.type, Enum): - _type = f"enum({_type.type.name})" - else: - _type = str(_type) - - _type = _type.replace(" memory", "") - _type = _type.replace(" storage ref", "") - - if "struct" in _type: - return "struct" - if "enum" in _type: - return "enum" - if "tuple" in _type: - return "tuple" - if "contract" in _type: - return "contract" - if "mapping" in _type: - return "mapping" - return _type.replace(" ", "_") - - -# pylint: disable=too-many-branches -def encode_ir_for_compare(ir: Operation) -> str: - # operations - if isinstance(ir, Assignment): - return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})" - if isinstance(ir, Index): - return f"index({ntype(ir.variable_right.type)})" - if isinstance(ir, Member): - return "member" # .format(ntype(ir._type)) - if isinstance(ir, Length): - return "length" - if isinstance(ir, Binary): - return f"binary({encode_var_for_compare(ir.variable_left)}{ir.type}{encode_var_for_compare(ir.variable_right)})" - if isinstance(ir, Unary): - return f"unary({str(ir.type)})" - if isinstance(ir, Condition): - return f"condition({encode_var_for_compare(ir.value)})" - if isinstance(ir, NewStructure): - return "new_structure" - if isinstance(ir, NewContract): - return "new_contract" - if isinstance(ir, NewArray): - return f"new_array({ntype(ir.array_type)})" - if isinstance(ir, NewElementaryType): - return f"new_elementary({ntype(ir.type)})" - if isinstance(ir, Delete): - return f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})" - if isinstance(ir, SolidityCall): - return f"solidity_call({ir.function.full_name})" - if isinstance(ir, InternalCall): - return f"internal_call({ntype(ir.type_call)})" - if isinstance(ir, EventCall): # is this useful? - return "event" - if isinstance(ir, LibraryCall): - return "library_call" - if isinstance(ir, InternalDynamicCall): - return "internal_dynamic_call" - if isinstance(ir, HighLevelCall): # TODO: improve - return "high_level_call" - if isinstance(ir, LowLevelCall): # TODO: improve - return "low_level_call" - if isinstance(ir, TypeConversion): - return f"type_conversion({ntype(ir.type)})" - if isinstance(ir, Return): # this can be improved using values - return "return" # .format(ntype(ir.type)) - if isinstance(ir, Transfer): - return f"transfer({encode_var_for_compare(ir.call_value)})" - if isinstance(ir, Send): - return f"send({encode_var_for_compare(ir.call_value)})" - if isinstance(ir, Unpack): # TODO: improve - return "unpack" - if isinstance(ir, InitArray): # TODO: improve - return "init_array" - - # default - return "" - - -# pylint: disable=too-many-branches -def encode_var_for_compare(var: Variable) -> str: - - # variables - if isinstance(var, Constant): - return f"constant({ntype(var.type)},{var.value})" - if isinstance(var, SolidityVariableComposed): - return f"solidity_variable_composed({var.name})" - if isinstance(var, SolidityVariable): - return f"solidity_variable{var.name}" - if isinstance(var, TemporaryVariable): - return "temporary_variable" - if isinstance(var, ReferenceVariable): - return f"reference({ntype(var.type)})" - if isinstance(var, LocalVariable): - return f"local_solc_variable({ntype(var.type)},{var.location})" - if isinstance(var, StateVariable): - if not (var.is_constant or var.is_immutable): - try: - slot, _ = var.contract.compilation_unit.storage_layout_of(var.contract, var) - except KeyError: - slot = var.name - else: - slot = var.name - return f"state_solc_variable({ntype(var.type)},{slot})" - if isinstance(var, LocalVariableInitFromTuple): - return "local_variable_init_tuple" - if isinstance(var, TupleVariable): - return "tuple_variable" - - # default - return "" - - def get_proxy_implementation_slot(proxy: Contract) -> Optional[SlotInfo]: """ Gets information about the storage slot where a proxy's implementation address is stored. diff --git a/slither/visitors/slithir/expression_to_slithir.py b/slither/visitors/slithir/expression_to_slithir.py index 1a2a85b2d2..0c439151b9 100644 --- a/slither/visitors/slithir/expression_to_slithir.py +++ b/slither/visitors/slithir/expression_to_slithir.py @@ -51,6 +51,7 @@ Member, TypeConversion, Unary, + UnaryType, Unpack, Return, SolidityCall, @@ -110,6 +111,13 @@ def set_val(expression: Expression, val: Any) -> None: BinaryOperationType.OROR: BinaryType.OROR, } + +_unary_to_unary = { + UnaryOperationType.BANG: UnaryType.BANG, + UnaryOperationType.TILD: UnaryType.TILD, +} + + _signed_to_unsigned = { BinaryOperationType.DIVISION_SIGNED: BinaryType.DIVISION, BinaryOperationType.MODULO_SIGNED: BinaryType.MODULO, @@ -335,7 +343,9 @@ def _post_call_expression(self, expression: CallExpression) -> None: val = TupleVariable(self._node) else: val = TemporaryVariable(self._node) - internal_call = InternalCall(called, len(args), val, expression.type_call) + internal_call = InternalCall( + called, len(args), val, expression.type_call, names=expression.names + ) internal_call.set_expression(expression) self._result.append(internal_call) set_val(expression, val) @@ -404,7 +414,9 @@ def _post_call_expression(self, expression: CallExpression) -> None: else: val = TemporaryVariable(self._node) - message_call = TmpCall(called, len(args), val, expression.type_call) + message_call = TmpCall( + called, len(args), val, expression.type_call, names=expression.names + ) message_call.set_expression(expression) # Gas/value are only accessible here if the syntax {gas: , value: } # Is used over .gas().value() @@ -490,6 +502,7 @@ def _post_member_access(self, expression: MemberAccess) -> None: # Look for type(X).max / min # Because we looked at the AST structure, we need to look into the nested expression # Hopefully this is always on a direct sub field, and there is no weird construction + # pylint: disable=too-many-nested-blocks if isinstance(expression.expression, CallExpression) and expression.member_name in [ "min", "max", @@ -509,10 +522,22 @@ def _post_member_access(self, expression: MemberAccess) -> None: constant_type = type_found else: # type(enum).max/min - assert isinstance(type_expression_found, Identifier) - type_found_in_expression = type_expression_found.value - assert isinstance(type_found_in_expression, (EnumContract, EnumTopLevel)) - type_found = UserDefinedType(type_found_in_expression) + # Case when enum is in another contract e.g. type(C.E).max + if isinstance(type_expression_found, MemberAccess): + contract = type_expression_found.expression.value + assert isinstance(contract, Contract) + for enum in contract.enums: + if enum.name == type_expression_found.member_name: + type_found_in_expression = enum + type_found = UserDefinedType(enum) + break + else: + assert isinstance(type_expression_found, Identifier) + type_found_in_expression = type_expression_found.value + assert isinstance( + type_found_in_expression, (EnumContract, EnumTopLevel) + ) + type_found = UserDefinedType(type_found_in_expression) constant_type = None min_value = type_found_in_expression.min max_value = type_found_in_expression.max @@ -563,13 +588,17 @@ def _post_member_access(self, expression: MemberAccess) -> None: # contract A { type MyInt is int} # contract B { function f() public{ A.MyInt test = A.MyInt.wrap(1);}} # The logic is handled by _post_call_expression - if expression.member_name in expr.file_scope.user_defined_types: - set_val(expression, expr.file_scope.user_defined_types[expression.member_name]) + if expression.member_name in expr.file_scope.type_aliases: + set_val(expression, expr.file_scope.type_aliases[expression.member_name]) return # Lookup errors referred to as member of contract e.g. Test.myError.selector if expression.member_name in expr.custom_errors_as_dict: set_val(expression, expr.custom_errors_as_dict[expression.member_name]) return + # Lookup enums when in a different contract e.g. C.E + if str(expression) in expr.enums_as_dict: + set_val(expression, expr.enums_as_dict[str(expression)]) + return val_ref = ReferenceVariable(self._node) member = Member(expr, Constant(expression.member_name), val_ref) @@ -632,7 +661,7 @@ def _post_unary_operation(self, expression: UnaryOperation) -> None: operation: Operation if expression.type in [UnaryOperationType.BANG, UnaryOperationType.TILD]: lvalue = TemporaryVariable(self._node) - operation = Unary(lvalue, value, expression.type) + operation = Unary(lvalue, value, _unary_to_unary[expression.type]) operation.set_expression(expression) self._result.append(operation) set_val(expression, lvalue) diff --git a/tests/e2e/detectors/snapshots/detectors__detector_IncorrectOperatorExponentiation_0_7_6_incorrect_exp_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_IncorrectOperatorExponentiation_0_7_6_incorrect_exp_sol__0.txt new file mode 100644 index 0000000000..458dd3bdcd --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_IncorrectOperatorExponentiation_0_7_6_incorrect_exp_sol__0.txt @@ -0,0 +1,9 @@ +Test.bad1() (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#9-12) has bitwise-xor operator ^ instead of the exponentiation operator **: + - UINT_MAX = 2 ^ 256 - 1 (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#10) + +Test.bad0(uint256) (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#5-7) has bitwise-xor operator ^ instead of the exponentiation operator **: + - a ^ 2 (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#6) + +Derived.slitherConstructorVariables() (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#30) has bitwise-xor operator ^ instead of the exponentiation operator **: + - my_var = 2 ^ 256 - 1 (tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol#3) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_IncorrectReturn_0_8_10_incorrect_return_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_IncorrectReturn_0_8_10_incorrect_return_sol__0.txt new file mode 100644 index 0000000000..5d87cd932d --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_IncorrectReturn_0_8_10_incorrect_return_sol__0.txt @@ -0,0 +1,4 @@ +C.bad1() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#21-24) calls C.indirect() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#17-19) which halt the execution return(uint256,uint256)(5,6) (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#4) + +C.bad0() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#8-11) calls C.internal_return() (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#2-6) which halt the execution return(uint256,uint256)(5,6) (tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol#4) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_4_25_MappingDeletion_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_4_25_MappingDeletion_sol__0.txt index 902f966688..4d47bb5709 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_4_25_MappingDeletion_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_4_25_MappingDeletion_sol__0.txt @@ -1,6 +1,9 @@ +Balances.deleteNestedBalance() (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#40-42) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#17-20) which contains a mapping: + -delete nestedStackBalance (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#41) + Lib.deleteSt(Lib.MyStruct[1]) (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#9-11) deletes Lib.MyStruct (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#5-7) which contains a mapping: -delete st[0] (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#10) -Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#28-31) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#17-20) which contains a mapping: - -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#30) +Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#35-38) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#17-20) which contains a mapping: + -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol#37) diff --git a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_5_16_MappingDeletion_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_5_16_MappingDeletion_sol__0.txt index fec236e1c2..88e4ac554f 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_5_16_MappingDeletion_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_5_16_MappingDeletion_sol__0.txt @@ -1,6 +1,9 @@ Lib.deleteSt(Lib.MyStruct[1]) (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#9-11) deletes Lib.MyStruct (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#5-7) which contains a mapping: -delete st[0] (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#10) -Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#29-32) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#17-20) which contains a mapping: - -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#31) +Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#35-38) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#17-20) which contains a mapping: + -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#37) + +Balances.deleteNestedBalance() (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#40-42) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#17-20) which contains a mapping: + -delete nestedStackBalance (tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol#41) diff --git a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_6_11_MappingDeletion_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_6_11_MappingDeletion_sol__0.txt index 7f0372c36d..4270f0d86c 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_6_11_MappingDeletion_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_6_11_MappingDeletion_sol__0.txt @@ -1,6 +1,9 @@ -Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#29-32) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#17-20) which contains a mapping: - -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#31) +Balances.deleteNestedBalance() (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#40-42) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#17-20) which contains a mapping: + -delete nestedStackBalance (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#41) Lib.deleteSt(Lib.MyStruct[1]) (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#9-11) deletes Lib.MyStruct (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#5-7) which contains a mapping: -delete st[0] (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#10) +Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#35-38) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#17-20) which contains a mapping: + -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol#37) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_7_6_MappingDeletion_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_7_6_MappingDeletion_sol__0.txt index f519a046f1..ea6ed2dd6a 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_7_6_MappingDeletion_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_MappingDeletionDetection_0_7_6_MappingDeletion_sol__0.txt @@ -1,5 +1,8 @@ -Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#29-32) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#17-20) which contains a mapping: - -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#31) +Balances.deleteNestedBalance() (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#40-42) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#17-20) which contains a mapping: + -delete nestedStackBalance (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#41) + +Balances.deleteBalance(uint256) (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#35-38) deletes Balances.BalancesStruct (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#17-20) which contains a mapping: + -delete stackBalance[idx] (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#37) Lib.deleteSt(Lib.MyStruct[1]) (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#9-11) deletes Lib.MyStruct (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#5-7) which contains a mapping: -delete st[0] (tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol#10) diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReturnBomb_0_8_20_return_bomb_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReturnBomb_0_8_20_return_bomb_sol__0.txt new file mode 100644 index 0000000000..f53b5233fd --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReturnBomb_0_8_20_return_bomb_sol__0.txt @@ -0,0 +1,5 @@ +Mark.oops(address) (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#31-55) tries to limit the gas of an external call that controls implicit decoding + ret1 = BadGuy(badGuy).fbad{gas: 2000}() (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#42) + (x,str) = BadGuy(badGuy).fbad1{gas: 2000}() (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#44) + (success,ret) = badGuy.call{gas: 10000}(abi.encodeWithSelector(BadGuy.llbad.selector)) (tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol#47-51) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReturnInsteadOfLeave_0_8_10_incorrect_return_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReturnInsteadOfLeave_0_8_10_incorrect_return_sol__0.txt new file mode 100644 index 0000000000..28f579f818 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReturnInsteadOfLeave_0_8_10_incorrect_return_sol__0.txt @@ -0,0 +1,2 @@ +C.f() (tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol#3-7) contains an incorrect call to return: return(uint256,uint256)(5,6) (tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol#5) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_TautologicalCompare_0_8_20_compare_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_TautologicalCompare_0_8_20_compare_sol__0.txt new file mode 100644 index 0000000000..76685043ce --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_TautologicalCompare_0_8_20_compare_sol__0.txt @@ -0,0 +1,3 @@ +A.check(uint256) (tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol#3-5) compares a variable to itself: + (a >= a) (tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol#4) + diff --git a/tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol b/tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol new file mode 100644 index 0000000000..b930ee5c1f --- /dev/null +++ b/tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol @@ -0,0 +1,30 @@ +contract Test { + + uint my_var = 2 ^ 256-1; + + function bad0(uint a) internal returns (uint) { + return a^2; + } + + function bad1() internal returns (uint) { + uint UINT_MAX = 2^256-1; + return UINT_MAX; + } + + /* Correct exponentiation operator */ + function good0(uint a) internal returns (uint) { + return a**2; + } + + /* Neither operand is a constant */ + function good1(uint a) internal returns (uint) { + return a^a; + } + + /* The constant operand 0xff in hex typically means bitwise xor */ + function good2(uint a) internal returns (uint) { + return a^0xff; + } +} + +contract Derived is Test {} diff --git a/tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol-0.7.6.zip b/tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol-0.7.6.zip new file mode 100644 index 0000000000..c6fea01511 Binary files /dev/null and b/tests/e2e/detectors/test_data/incorrect-exp/0.7.6/incorrect_exp.sol-0.7.6.zip differ diff --git a/tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol b/tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol new file mode 100644 index 0000000000..e81a747ba1 --- /dev/null +++ b/tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol @@ -0,0 +1,36 @@ +contract C { + function internal_return() internal{ + assembly { + return (5, 6) + } + } + + function bad0() public returns (bool){ + internal_return(); + return true; + } + + function indirect2() internal { + internal_return(); + } + + function indirect() internal { + indirect2(); + } + + function bad1() public returns (bool){ + indirect(); + return true; + } + + function good0() public{ + // Dont report if there is no following operation + internal_return(); + } + + function good1() public returns (uint a, uint b){ + assembly { + return (5, 6) + } + } +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol-0.8.10.zip b/tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol-0.8.10.zip new file mode 100644 index 0000000000..2491a10a65 Binary files /dev/null and b/tests/e2e/detectors/test_data/incorrect-return/0.8.10/incorrect_return.sol-0.8.10.zip differ diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol b/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol index bedbb64a8f..bcbc86c9d1 100644 --- a/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol +++ b/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol @@ -6,7 +6,7 @@ library Lib{ mapping(address => uint) maps; } - function deleteSt(MyStruct[1] storage st){ + function deleteSt(MyStruct[1] storage st) internal { delete st[0]; } @@ -17,18 +17,29 @@ contract Balances { struct BalancesStruct{ address owner; mapping(address => uint) balances; - } + } + + struct NestedBalanceStruct { + BalancesStruct balanceStruct; + } mapping(uint => BalancesStruct) public stackBalance; + NestedBalanceStruct internal nestedStackBalance; + function createBalance(uint idx) public { - require(stackBalance[idx].owner == 0); - stackBalance[idx] = BalancesStruct(msg.sender); + require(stackBalance[idx].owner == address(0)); + BalancesStruct storage str = stackBalance[idx]; + str.owner = msg.sender; } function deleteBalance(uint idx) public { require(stackBalance[idx].owner == msg.sender); delete stackBalance[idx]; } + + function deleteNestedBalance() public { + delete nestedStackBalance; + } function setBalance(uint idx, address addr, uint val) public { require(stackBalance[idx].owner == msg.sender); diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol-0.4.25.zip b/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol-0.4.25.zip index be3e13807a..5885936995 100644 Binary files a/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol-0.4.25.zip and b/tests/e2e/detectors/test_data/mapping-deletion/0.4.25/MappingDeletion.sol-0.4.25.zip differ diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol b/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol index e1b608a463..bcbc86c9d1 100644 --- a/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol +++ b/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol @@ -17,9 +17,15 @@ contract Balances { struct BalancesStruct{ address owner; mapping(address => uint) balances; - } + } + + struct NestedBalanceStruct { + BalancesStruct balanceStruct; + } mapping(uint => BalancesStruct) public stackBalance; + NestedBalanceStruct internal nestedStackBalance; + function createBalance(uint idx) public { require(stackBalance[idx].owner == address(0)); BalancesStruct storage str = stackBalance[idx]; @@ -30,6 +36,10 @@ contract Balances { require(stackBalance[idx].owner == msg.sender); delete stackBalance[idx]; } + + function deleteNestedBalance() public { + delete nestedStackBalance; + } function setBalance(uint idx, address addr, uint val) public { require(stackBalance[idx].owner == msg.sender); diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol-0.5.16.zip b/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol-0.5.16.zip index 0ad84a5889..2e57890461 100644 Binary files a/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol-0.5.16.zip and b/tests/e2e/detectors/test_data/mapping-deletion/0.5.16/MappingDeletion.sol-0.5.16.zip differ diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol b/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol index e1b608a463..bcbc86c9d1 100644 --- a/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol +++ b/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol @@ -17,9 +17,15 @@ contract Balances { struct BalancesStruct{ address owner; mapping(address => uint) balances; - } + } + + struct NestedBalanceStruct { + BalancesStruct balanceStruct; + } mapping(uint => BalancesStruct) public stackBalance; + NestedBalanceStruct internal nestedStackBalance; + function createBalance(uint idx) public { require(stackBalance[idx].owner == address(0)); BalancesStruct storage str = stackBalance[idx]; @@ -30,6 +36,10 @@ contract Balances { require(stackBalance[idx].owner == msg.sender); delete stackBalance[idx]; } + + function deleteNestedBalance() public { + delete nestedStackBalance; + } function setBalance(uint idx, address addr, uint val) public { require(stackBalance[idx].owner == msg.sender); diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol-0.6.11.zip b/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol-0.6.11.zip index 5f66da0617..8f532ea669 100644 Binary files a/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol-0.6.11.zip and b/tests/e2e/detectors/test_data/mapping-deletion/0.6.11/MappingDeletion.sol-0.6.11.zip differ diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol b/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol index e1b608a463..bcbc86c9d1 100644 --- a/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol +++ b/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol @@ -17,9 +17,15 @@ contract Balances { struct BalancesStruct{ address owner; mapping(address => uint) balances; - } + } + + struct NestedBalanceStruct { + BalancesStruct balanceStruct; + } mapping(uint => BalancesStruct) public stackBalance; + NestedBalanceStruct internal nestedStackBalance; + function createBalance(uint idx) public { require(stackBalance[idx].owner == address(0)); BalancesStruct storage str = stackBalance[idx]; @@ -30,6 +36,10 @@ contract Balances { require(stackBalance[idx].owner == msg.sender); delete stackBalance[idx]; } + + function deleteNestedBalance() public { + delete nestedStackBalance; + } function setBalance(uint idx, address addr, uint val) public { require(stackBalance[idx].owner == msg.sender); diff --git a/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol-0.7.6.zip b/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol-0.7.6.zip index 5888e0e534..c685454ce6 100644 Binary files a/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol-0.7.6.zip and b/tests/e2e/detectors/test_data/mapping-deletion/0.7.6/MappingDeletion.sol-0.7.6.zip differ diff --git a/tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol b/tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol new file mode 100644 index 0000000000..76413fdcaa --- /dev/null +++ b/tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol @@ -0,0 +1,57 @@ +contract BadGuy { + function llbad() external pure returns (bytes memory) { + assembly{ + revert(0, 1000000) + } + } + + function fgood() external payable returns (uint){ + assembly{ + return(0, 1000000) + } + } + + function fbad() external payable returns (uint[] memory){ + assembly{ + return(0, 1000000) + } + } + + function fbad1() external payable returns (uint, string memory){ + assembly{ + return(0, 1000000) + } + } + + +} + +contract Mark { + + function oops(address badGuy) public{ + bool success; + string memory str; + bytes memory ret; + uint x; + uint[] memory ret1; + + x = BadGuy(badGuy).fgood{gas:2000}(); + + ret1 = BadGuy(badGuy).fbad(); //good (no gas specified) + + ret1 = BadGuy(badGuy).fbad{gas:2000}(); + + (x, str) = BadGuy(badGuy).fbad1{gas:2000}(); + + // Mark pays a lot of gas for this copy 😬😬😬 + (success, ret) = badGuy.call{gas:10000}( + abi.encodeWithSelector( + BadGuy.llbad.selector + ) + ); + + // Mark may OOG here, preventing local state changes + //importantCleanup(); + } +} + diff --git a/tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol-0.8.20.zip b/tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol-0.8.20.zip new file mode 100644 index 0000000000..4c10ea6fea Binary files /dev/null and b/tests/e2e/detectors/test_data/return-bomb/0.8.20/return_bomb.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol b/tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol new file mode 100644 index 0000000000..f02d7f3791 --- /dev/null +++ b/tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol @@ -0,0 +1,8 @@ +contract C { + + function f() internal returns (uint a, uint b){ + assembly { + return (5, 6) + } + } +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol-0.8.10.zip b/tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol-0.8.10.zip new file mode 100644 index 0000000000..f4c389fcfb Binary files /dev/null and b/tests/e2e/detectors/test_data/return-leave/0.8.10/incorrect_return.sol-0.8.10.zip differ diff --git a/tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol b/tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol new file mode 100644 index 0000000000..ede2f0fc18 --- /dev/null +++ b/tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol @@ -0,0 +1,6 @@ + +contract A{ + function check(uint a) external returns(bool){ + return (a >= a); + } +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol-0.8.20.zip b/tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol-0.8.20.zip new file mode 100644 index 0000000000..05b78fad08 Binary files /dev/null and b/tests/e2e/detectors/test_data/tautological-compare/0.8.20/compare.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 6fc04e4e1f..28dcc5e755 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1654,6 +1654,31 @@ def id_test(test_item: Test): "encode_packed_collision.sol", "0.7.6", ), + Test( + all_detectors.IncorrectReturn, + "incorrect_return.sol", + "0.8.10", + ), + Test( + all_detectors.ReturnInsteadOfLeave, + "incorrect_return.sol", + "0.8.10", + ), + Test( + all_detectors.IncorrectOperatorExponentiation, + "incorrect_exp.sol", + "0.7.6", + ), + Test( + all_detectors.TautologicalCompare, + "compare.sol", + "0.8.20", + ), + Test( + all_detectors.ReturnBomb, + "return_bomb.sol", + "0.8.20", + ), ] GENERIC_PATH = "/GENERIC_PATH" diff --git a/tests/e2e/solc_parsing/test_ast_parsing.py b/tests/e2e/solc_parsing/test_ast_parsing.py index 307e6736ff..37f6d7274b 100644 --- a/tests/e2e/solc_parsing/test_ast_parsing.py +++ b/tests/e2e/solc_parsing/test_ast_parsing.py @@ -4,13 +4,13 @@ import sys from pathlib import Path from typing import List, Dict, Tuple -from packaging.version import parse as parse_version -import pytest + +from crytic_compile import CryticCompile, save_to_zip +from crytic_compile.utils.zip import load_from_zip from deepdiff import DeepDiff +from packaging.version import parse as parse_version from solc_select.solc_select import install_artifacts as install_solc_versions from solc_select.solc_select import installed_versions as get_installed_solc_versions -from crytic_compile import CryticCompile, save_to_zip -from crytic_compile.utils.zip import load_from_zip from slither import Slither from slither.printers.guidance.echidna import Echidna @@ -459,6 +459,8 @@ def make_version(minor: int, patch_min: int, patch_max: int) -> List[str]: ["0.6.9", "0.7.6", "0.8.16"], ), Test("user_defined_operators-0.8.19.sol", ["0.8.19"]), + Test("type-aliases.sol", ["0.8.19"]), + Test("enum-max-min.sol", ["0.8.19"]), ] # create the output folder if needed try: diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.10-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.10-compact.zip index fc45cb40e2..c88ab3209e 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.10-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.10-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.11-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.11-compact.zip index 853879cf56..f49cefb4fa 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.11-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.11-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.12-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.12-compact.zip index c72629231a..11e9f5c769 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.12-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.12-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.13-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.13-compact.zip index 9ada2c3351..bfe3abf69d 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.13-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.13-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.14-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.14-compact.zip index e36f5b94e5..fcb0fd1f45 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.14-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.14-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.15-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.15-compact.zip index 1023ab1d45..2050e256ab 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.15-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.15-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.4-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.4-compact.zip index 5ecf2b037a..d31650594c 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.4-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.4-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.5-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.5-compact.zip index 357554c09b..a5551ea094 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.5-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.5-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.6-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.6-compact.zip index c5f05ef201..006cd15705 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.6-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.6-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.7-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.7-compact.zip index 51828bce7f..95f411a302 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.7-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.7-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.8-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.8-compact.zip index eb6ba091b4..f0da5dfc49 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.8-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.8-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.9-compact.zip b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.9-compact.zip index 17e7520185..f407956184 100644 Binary files a/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.9-compact.zip and b/tests/e2e/solc_parsing/test_data/compile/custom_error-0.8.4.sol-0.8.9-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/enum-max-min.sol-0.8.19-compact.zip b/tests/e2e/solc_parsing/test_data/compile/enum-max-min.sol-0.8.19-compact.zip new file mode 100644 index 0000000000..f29ad0bdb6 Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/enum-max-min.sol-0.8.19-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/type-aliases.sol-0.8.19-compact.zip b/tests/e2e/solc_parsing/test_data/compile/type-aliases.sol-0.8.19-compact.zip new file mode 100644 index 0000000000..0c4d2b1c5e Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/type-aliases.sol-0.8.19-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/custom_error-0.8.4.sol b/tests/e2e/solc_parsing/test_data/custom_error-0.8.4.sol index 4e1e388fb1..e638b8d41c 100644 --- a/tests/e2e/solc_parsing/test_data/custom_error-0.8.4.sol +++ b/tests/e2e/solc_parsing/test_data/custom_error-0.8.4.sol @@ -8,13 +8,19 @@ struct St{ uint v; } +uint256 constant MAX = 5; + error ErrorSimple(); error ErrorWithArgs(uint, uint); error ErrorWithStruct(St s); +error ErrorWithConst(uint256[MAX]); contract VendingMachine is I { + uint256 constant CMAX = 10; + error CErrorWithConst(uint256[CMAX]); + function err0() public { revert ErrorSimple(); } @@ -32,6 +38,14 @@ contract VendingMachine is I { function err4() public { revert ErrorWithEnum(SomeEnum.ONE); } + function err5(uint256[MAX] calldata a) public { + revert ErrorWithConst(a); + } + function err6(uint256[CMAX] calldata a) public { + revert CErrorWithConst(a); + } + + } contract A{ diff --git a/tests/e2e/solc_parsing/test_data/enum-max-min.sol b/tests/e2e/solc_parsing/test_data/enum-max-min.sol new file mode 100644 index 0000000000..5f5ecc342f --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/enum-max-min.sol @@ -0,0 +1,37 @@ + +library Q { + enum E {a} +} + +contract Z { + enum E {a,b} +} + +contract D { + enum E {a,b,c} + + function a() public returns(uint){ + return uint(type(E).max); + } + + function b() public returns(uint){ + return uint(type(Q.E).max); + } + + function c() public returns(uint){ + return uint(type(Z.E).max); + } + + function d() public returns(uint){ + return uint(type(E).min); + } + + function e() public returns(uint){ + return uint(type(Q.E).min); + } + + function f() public returns(uint){ + return uint(type(Z.E).min); + } + +} diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.10-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.10-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.10-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.10-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.11-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.11-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.11-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.11-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.12-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.12-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.12-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.12-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.13-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.13-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.13-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.13-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.14-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.14-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.14-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.14-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.15-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.15-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.15-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.15-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.4-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.4-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.4-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.4-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.5-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.5-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.5-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.5-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.6-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.6-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.6-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.6-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.7-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.7-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.7-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.7-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.8-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.8-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.8-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.8-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.9-compact.json b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.9-compact.json index 9020c8d522..0707fa1498 100644 --- a/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.9-compact.json +++ b/tests/e2e/solc_parsing/test_data/expected/custom_error-0.8.4.sol-0.8.9-compact.json @@ -5,7 +5,9 @@ "err1()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err2()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n", "err3()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", - "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + "err4()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err5(uint256[5])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "err6(uint256[10])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" }, "A": { "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" diff --git a/tests/e2e/solc_parsing/test_data/expected/enum-max-min.sol-0.8.19-compact.json b/tests/e2e/solc_parsing/test_data/expected/enum-max-min.sol-0.8.19-compact.json new file mode 100644 index 0000000000..db0ebc88be --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/expected/enum-max-min.sol-0.8.19-compact.json @@ -0,0 +1,12 @@ +{ + "Q": {}, + "Z": {}, + "D": { + "a()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n", + "b()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n", + "c()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n", + "d()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n", + "e()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n", + "f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n" + } +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/expected/type-aliases.sol-0.8.19-compact.json b/tests/e2e/solc_parsing/test_data/expected/type-aliases.sol-0.8.19-compact.json new file mode 100644 index 0000000000..e76cc99ffd --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/expected/type-aliases.sol-0.8.19-compact.json @@ -0,0 +1,6 @@ +{ + "OtherTest": { + "myfunc()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n}\n" + }, + "DeleteTest": {} +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/type-aliases.sol b/tests/e2e/solc_parsing/test_data/type-aliases.sol new file mode 100644 index 0000000000..53fdaabeb8 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/type-aliases.sol @@ -0,0 +1,20 @@ + +struct Z { + int x; + int y; +} + +contract OtherTest { + struct Z { + int x; + int y; + } + + function myfunc() external { + Z memory z = Z(2,3); + } +} + +contract DeleteTest { + type Z is int; +} diff --git a/tests/unit/core/test_source_mapping.py b/tests/unit/core/test_source_mapping.py index 552c08dc95..9577014297 100644 --- a/tests/unit/core/test_source_mapping.py +++ b/tests/unit/core/test_source_mapping.py @@ -85,15 +85,13 @@ def test_references_user_defined_aliases(solc_binary_path): file = Path(SRC_MAPPING_TEST_ROOT, "ReferencesUserDefinedAliases.sol").as_posix() slither = Slither(file, solc=solc_path) - alias_top_level = slither.compilation_units[0].user_defined_value_types["aliasTopLevel"] + alias_top_level = slither.compilation_units[0].type_aliases["aliasTopLevel"] assert len(alias_top_level.references) == 2 lines = _sort_references_lines(alias_top_level.references) assert lines == [12, 16] alias_contract_level = ( - slither.compilation_units[0] - .contracts[0] - .file_scope.user_defined_types["C.aliasContractLevel"] + slither.compilation_units[0].contracts[0].file_scope.type_aliases["C.aliasContractLevel"] ) assert len(alias_contract_level.references) == 2 lines = _sort_references_lines(alias_contract_level.references) diff --git a/tests/unit/slithir/test_argument_reorder.py b/tests/unit/slithir/test_argument_reorder.py new file mode 100644 index 0000000000..12f5bd7f28 --- /dev/null +++ b/tests/unit/slithir/test_argument_reorder.py @@ -0,0 +1,65 @@ +from pathlib import Path + +from slither import Slither +from slither.slithir.operations.internal_call import InternalCall +from slither.slithir.operations.new_structure import NewStructure +from slither.slithir.variables.constant import Constant + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" +ARG_REORDER_TEST_ROOT = Path(TEST_DATA_DIR, "argument_reorder") + + +def test_struct_constructor_reorder(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") + slither = Slither( + Path(ARG_REORDER_TEST_ROOT, "test_struct_constructor.sol").as_posix(), solc=solc_path + ) + + operations = slither.contracts[0].functions[0].slithir_operations + constructor_calls = [x for x in operations if isinstance(x, NewStructure)] + assert len(constructor_calls) == 2 + + # Arguments to first call are 2, 3 + assert ( + isinstance(constructor_calls[0].arguments[0], Constant) + and constructor_calls[0].arguments[0].value == 2 + ) + assert ( + isinstance(constructor_calls[0].arguments[1], Constant) + and constructor_calls[0].arguments[1].value == 3 + ) + + # Arguments to second call are 5, 4 (note the reversed order) + assert ( + isinstance(constructor_calls[1].arguments[0], Constant) + and constructor_calls[1].arguments[0].value == 5 + ) + assert ( + isinstance(constructor_calls[1].arguments[1], Constant) + and constructor_calls[1].arguments[1].value == 4 + ) + + +def test_internal_call_reorder(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") + slither = Slither( + Path(ARG_REORDER_TEST_ROOT, "test_internal_call_reorder.sol").as_posix(), solc=solc_path + ) + + operations = slither.contracts[0].functions[1].slithir_operations + internal_calls = [x for x in operations if isinstance(x, InternalCall)] + assert len(internal_calls) == 1 + + # Arguments to call are 3, true, 5 + assert ( + isinstance(internal_calls[0].arguments[0], Constant) + and internal_calls[0].arguments[0].value == 3 + ) + assert ( + isinstance(internal_calls[0].arguments[1], Constant) + and internal_calls[0].arguments[1].value is True + ) + assert ( + isinstance(internal_calls[0].arguments[2], Constant) + and internal_calls[0].arguments[2].value == 5 + ) diff --git a/tests/unit/slithir/test_data/argument_reorder/test_internal_call_reorder.sol b/tests/unit/slithir/test_data/argument_reorder/test_internal_call_reorder.sol new file mode 100644 index 0000000000..4c4f658da8 --- /dev/null +++ b/tests/unit/slithir/test_data/argument_reorder/test_internal_call_reorder.sol @@ -0,0 +1,8 @@ +contract InternalCallReorderTest { + function internal_func(uint256 a, bool b, uint256 c) internal { + } + + function caller() external { + internal_func({a: 3, c: 5, b: true}); + } +} diff --git a/tests/unit/slithir/test_data/argument_reorder/test_struct_constructor.sol b/tests/unit/slithir/test_data/argument_reorder/test_struct_constructor.sol new file mode 100644 index 0000000000..05dbb50b0f --- /dev/null +++ b/tests/unit/slithir/test_data/argument_reorder/test_struct_constructor.sol @@ -0,0 +1,11 @@ +contract StructConstructorTest { + struct S { + int x; + int y; + } + + function test() external { + S memory p = S({x: 2, y: 3}); + S memory q = S({y: 4, x: 5}); + } +} diff --git a/tests/unit/slithir/test_data/enum_max_min.sol b/tests/unit/slithir/test_data/enum_max_min.sol new file mode 100644 index 0000000000..5f5ecc342f --- /dev/null +++ b/tests/unit/slithir/test_data/enum_max_min.sol @@ -0,0 +1,37 @@ + +library Q { + enum E {a} +} + +contract Z { + enum E {a,b} +} + +contract D { + enum E {a,b,c} + + function a() public returns(uint){ + return uint(type(E).max); + } + + function b() public returns(uint){ + return uint(type(Q.E).max); + } + + function c() public returns(uint){ + return uint(type(Z.E).max); + } + + function d() public returns(uint){ + return uint(type(E).min); + } + + function e() public returns(uint){ + return uint(type(Q.E).min); + } + + function f() public returns(uint){ + return uint(type(Z.E).min); + } + +} diff --git a/tests/unit/slithir/test_data/ternary_expressions.sol b/tests/unit/slithir/test_data/ternary_expressions.sol index ebfb96e801..1ccd51d34d 100644 --- a/tests/unit/slithir/test_data/ternary_expressions.sol +++ b/tests/unit/slithir/test_data/ternary_expressions.sol @@ -1,6 +1,6 @@ interface Test { function test() external payable returns (uint); - function testTuple() external payable returns (uint, uint); + function testTuple(uint) external payable returns (uint, uint); } contract C { // TODO @@ -36,21 +36,23 @@ contract C { } // Unused tuple variable - function g(address one) public { - (, uint x) = Test(one).testTuple(); - } - uint[] myIntegers; - function _h(uint c) internal returns(uint) { - return c; - } - function h(bool cond, uint a, uint b) public { - uint d = _h( - myIntegers[cond ? a : b] - ); + function g(address one, bool cond, uint a, uint b) public { + (, uint x) = Test(one).testTuple(myIntegers[cond ? a : b]); } - - function i(bool cond) public { + + function h(bool cond) public { bytes memory a = new bytes(cond ? 1 : 2); } } + +contract D { + function values(uint n) internal returns (uint, uint) { + return (0, 1); + } + + function a(uint n) external { + uint a; + (a,) = values(n > 0 ? 1 : 0); + } +} diff --git a/tests/unit/slithir/test_enum.py b/tests/unit/slithir/test_enum.py new file mode 100644 index 0000000000..4f1fc4e595 --- /dev/null +++ b/tests/unit/slithir/test_enum.py @@ -0,0 +1,67 @@ +from pathlib import Path +from slither import Slither +from slither.slithir.operations import Assignment +from slither.slithir.variables import Constant + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" + + +def test_enum_max_min(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.19") + slither = Slither(Path(TEST_DATA_DIR, "enum_max_min.sol").as_posix(), solc=solc_path) + + contract = slither.get_contract_from_name("D")[0] + + f = contract.get_function_from_full_name("a()") + # TMP_1(uint256) := 2(uint256) + assignment = f.slithir_operations[1] + assert ( + isinstance(assignment, Assignment) + and isinstance(assignment.rvalue, Constant) + and assignment.rvalue.value == 2 + ) + + f = contract.get_function_from_full_name("b()") + # TMP_4(uint256) := 0(uint256) + assignment = f.slithir_operations[1] + assert ( + isinstance(assignment, Assignment) + and isinstance(assignment.rvalue, Constant) + and assignment.rvalue.value == 0 + ) + + f = contract.get_function_from_full_name("c()") + # TMP_7(uint256) := 1(uint256) + assignment = f.slithir_operations[1] + assert ( + isinstance(assignment, Assignment) + and isinstance(assignment.rvalue, Constant) + and assignment.rvalue.value == 1 + ) + + f = contract.get_function_from_full_name("d()") + # TMP_10(uint256) := 0(uint256) + assignment = f.slithir_operations[1] + assert ( + isinstance(assignment, Assignment) + and isinstance(assignment.rvalue, Constant) + and assignment.rvalue.value == 0 + ) + + f = contract.get_function_from_full_name("e()") + # TMP_13(uint256) := 0(uint256) + assignment = f.slithir_operations[1] + assert ( + isinstance(assignment, Assignment) + and isinstance(assignment.rvalue, Constant) + and assignment.rvalue.value == 0 + ) + + f = contract.get_function_from_full_name("f()") + # TMP_16(uint256) := 0(uint256) + assignment = f.slithir_operations[1] + assert ( + isinstance(assignment, Assignment) + and isinstance(assignment.rvalue, Constant) + and assignment.rvalue.value == 0 + ) diff --git a/tests/unit/slithir/test_ssa_generation.py b/tests/unit/slithir/test_ssa_generation.py index 6b1a1d1023..688c0a5fb1 100644 --- a/tests/unit/slithir/test_ssa_generation.py +++ b/tests/unit/slithir/test_ssa_generation.py @@ -11,7 +11,7 @@ from slither import Slither from slither.core.cfg.node import Node, NodeType from slither.core.declarations import Function, Contract -from slither.core.solidity_types import ArrayType +from slither.core.solidity_types import ArrayType, ElementaryType from slither.core.variables.local_variable import LocalVariable from slither.core.variables.state_variable import StateVariable from slither.slithir.operations import ( @@ -1116,3 +1116,23 @@ def test_issue_1846_ternary_in_ternary(slither_from_solidity_source): assert node.type == NodeType.IF assert node.son_true.type == NodeType.IF assert node.son_false.type == NodeType.EXPRESSION + + +def test_issue_2016(slither_from_source): + source = """ + contract Contract { + function test() external { + int[] memory a = new int[](5); + } + } + """ + with slither_from_source(source) as slither: + c = slither.get_contract_from_name("Contract")[0] + f = c.functions[0] + operations = f.slithir_operations + new_op = operations[0] + lvalue = new_op.lvalue + lvalue_type = lvalue.type + assert isinstance(lvalue_type, ArrayType) + assert lvalue_type.type == ElementaryType("int256") + assert lvalue_type.is_dynamic diff --git a/tests/unit/slithir/test_ternary_expressions.py b/tests/unit/slithir/test_ternary_expressions.py index 712c9582b0..bf8556f85d 100644 --- a/tests/unit/slithir/test_ternary_expressions.py +++ b/tests/unit/slithir/test_ternary_expressions.py @@ -16,30 +16,53 @@ def test_ternary_conversions(solc_binary_path) -> None: """This tests that true and false sons define the same number of variables that the father node declares""" solc_path = solc_binary_path("0.8.0") slither = Slither(Path(TEST_DATA_DIR, "ternary_expressions.sol").as_posix(), solc=solc_path) - for contract in slither.contracts: - if not contract.is_signature_only: - for function in contract.functions: - vars_declared = 0 - vars_assigned = 0 - for node in function.nodes: - if node.type in [NodeType.IF, NodeType.IFLOOP]: - - # Iterate over true and false son - for inner_node in node.sons: - # Count all variables declared - expression = inner_node.expression - if isinstance( - expression, (AssignmentOperation, NewElementaryType, CallExpression) - ): - var_expr = expression.expression_left - # Only tuples declare more than one var - if isinstance(var_expr, TupleExpression): - vars_declared += len(var_expr.expressions) - else: - vars_declared += 1 - - for ir in inner_node.irs: - # Count all variables defined - if isinstance(ir, (Assignment, Unpack)): - vars_assigned += 1 - assert vars_declared == vars_assigned and vars_assigned != 0 + contract = next(c for c in slither.contracts if c.name == "C") + for function in contract.functions: + vars_declared = 0 + vars_assigned = 0 + for node in function.nodes: + if node.type in [NodeType.IF, NodeType.IFLOOP]: + + # Iterate over true and false son + for inner_node in node.sons: + # Count all variables declared + expression = inner_node.expression + if isinstance( + expression, (AssignmentOperation, NewElementaryType, CallExpression) + ): + var_expr = expression.expression_left + # Only tuples declare more than one var + if isinstance(var_expr, TupleExpression): + vars_declared += len(var_expr.expressions) + else: + vars_declared += 1 + + for ir in inner_node.irs: + # Count all variables defined + if isinstance(ir, (Assignment, Unpack)): + vars_assigned += 1 + assert vars_declared == vars_assigned and vars_assigned != 0 + + +def test_ternary_tuple(solc_binary_path) -> None: + """ + Test that in the ternary liftings of an assignment of the form `(z, ) = ...`, + we obtain `z` from an unpack operation in both lifitings + """ + solc_path = solc_binary_path("0.8.0") + slither = Slither(Path(TEST_DATA_DIR, "ternary_expressions.sol").as_posix(), solc=solc_path) + contract = next(c for c in slither.contracts if c.name == "D") + fn = next(f for f in contract.functions if f.name == "a") + + if_nodes = [n for n in fn.nodes if n.type == NodeType.IF] + assert len(if_nodes) == 1 + + if_node = if_nodes[0] + assert isinstance(if_node.son_true.expression, AssignmentOperation) + assert ( + len([ir for ir in if_node.son_true.all_slithir_operations() if isinstance(ir, Unpack)]) == 1 + ) + assert ( + len([ir for ir in if_node.son_false.all_slithir_operations() if isinstance(ir, Unpack)]) + == 1 + )