Merge branch 'crytic:dev' into dev

pull/1022/head
Tadashi 3 years ago committed by GitHub
commit 36b6476c32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .github/ISSUE_TEMPLATE/feature_request.yml
  2. 46
      .github/workflows/IR.yml
  3. 4
      .github/workflows/black.yml
  4. 45
      .github/workflows/ci.yml
  5. 12
      .github/workflows/detectors.yml
  6. 12
      .github/workflows/features.yml
  7. 4
      .github/workflows/linter.yml
  8. 18
      .github/workflows/parser.yml
  9. 3
      .github/workflows/pip-audit.yml
  10. 4
      .github/workflows/pylint.yml
  11. 3
      CONTRIBUTING.md
  12. 13
      scripts/ci_test_dapp.sh
  13. 6
      setup.py
  14. 2
      slither/__main__.py
  15. 7
      slither/core/declarations/custom_error.py
  16. 8
      slither/core/scope/scope.py
  17. 5
      slither/core/slither_core.py
  18. 1
      slither/core/solidity_types/__init__.py
  19. 6
      slither/core/solidity_types/elementary_type.py
  20. 41
      slither/core/solidity_types/type_alias.py
  21. 2
      slither/detectors/attributes/constant_pragma.py
  22. 2
      slither/detectors/attributes/unimplemented_interface.py
  23. 8
      slither/detectors/naming_convention/naming_convention.py
  24. 2
      slither/detectors/source/rtlo.py
  25. 2
      slither/detectors/statements/type_based_tautology.py
  26. 24
      slither/printers/guidance/echidna.py
  27. 6
      slither/slithir/convert.py
  28. 26
      slither/solc_parsing/declarations/contract.py
  29. 17
      slither/solc_parsing/expressions/find_variable.py
  30. 18
      slither/solc_parsing/slither_compilation_unit_solc.py
  31. 14
      slither/solc_parsing/solidity_types/type_parsing.py
  32. 1
      slither/solc_parsing/yul/parse_yul.py
  33. 2
      slither/utils/integer_conversion.py
  34. 2
      slither/visitors/expression/constants_folding.py
  35. 55
      slither/visitors/slithir/expression_to_slithir.py
  36. 3
      tests/.gitattributes
  37. 17
      tests/ast-parsing/bytes_call.sol
  38. BIN
      tests/ast-parsing/compile/bytes_call.sol-0.8.12-compact.zip
  39. BIN
      tests/ast-parsing/compile/user_defined_types.sol-0.8.10-compact.zip
  40. BIN
      tests/ast-parsing/compile/user_defined_types.sol-0.8.11-compact.zip
  41. BIN
      tests/ast-parsing/compile/user_defined_types.sol-0.8.12-compact.zip
  42. BIN
      tests/ast-parsing/compile/user_defined_types.sol-0.8.8-compact.zip
  43. 9
      tests/ast-parsing/expected/bytes_call.sol-0.8.12-compact.json
  44. 10
      tests/ast-parsing/expected/user_defined_types.sol-0.8.10-compact.json
  45. 10
      tests/ast-parsing/expected/user_defined_types.sol-0.8.11-compact.json
  46. 10
      tests/ast-parsing/expected/user_defined_types.sol-0.8.12-compact.json
  47. 10
      tests/ast-parsing/expected/user_defined_types.sol-0.8.8-compact.json
  48. 30
      tests/ast-parsing/user_defined_types.sol
  49. 6
      tests/detectors/pragma/0.4.25/pragma.0.4.25.sol.0.4.25.ConstantPragma.json
  50. 6
      tests/detectors/pragma/0.5.16/pragma.0.5.16.sol.0.5.16.ConstantPragma.json
  51. 6
      tests/detectors/pragma/0.6.11/pragma.0.6.11.sol.0.6.11.ConstantPragma.json
  52. 6
      tests/detectors/pragma/0.7.6/pragma.0.7.6.sol.0.7.6.ConstantPragma.json
  53. 3
      tests/test_ast_parsing.py
  54. 17
      tests/test_detectors.py
  55. 934
      tests/test_ssa_generation.py

@ -1,3 +1,4 @@
---
name: Feature request
description: Suggest a feature
labels: ["enhancement"]

@ -0,0 +1,46 @@
---
name: IR tests
defaults:
run:
# To load bashrc
shell: bash -ieo pipefail {0}
on:
pull_request:
branches: [master, dev]
schedule:
# run CI every day even if no PRs/merges occur
- cron: '0 12 * * *'
jobs:
build:
name: IR tests
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-2022]
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v3
with:
python-version: 3.6
- name: Install dependencies
run: |
python setup.py install
pip install deepdiff
pip install pytest
pip install "solc-select>=v1.0.0b1"
solc-select install all
solc-select use 0.8.11
- name: Test with pytest
run: |
pytest tests/test_ssa_generation.py

@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: 3.6
@ -36,7 +36,7 @@ jobs:
cp pyproject.toml .github/linters
- name: Black
uses: github/super-linter/slim@v4.8.7
uses: github/super-linter/slim@v4.9.2
if: always()
env:
# run linter on everything to catch preexisting problems

@ -1,3 +1,4 @@
---
name: CI
defaults:
@ -17,39 +18,71 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "windows-2022"]
type: ["cli",
"dapp",
"data_dependency",
# "embark",
"erc",
"etherlime",
# "etherscan"
"find_paths",
"flat",
"kspec",
"printers",
# "prop"
"simil",
"slither_config",
"truffle",
"upgradability",
# "prop",
"flat"]
"upgradability"]
exclude:
# Requires nix
- os: windows-2022
type: dapp
# Requires nvm
- os: windows-2022
type: etherlime
# Requires nvm
- os: windows-2022
type: truffle
steps:
- uses: actions/checkout@v1
- name: Set up shell
if: runner.os == 'Windows'
run: |
echo 'C:\msys64\mingw64\bin' >> "$GITHUB_PATH"
echo 'C:\msys64\usr\bin' >> "$GITHUB_PATH"
- name: Set up Python 3.6
uses: actions/setup-python@v1
uses: actions/setup-python@v3
with:
python-version: 3.6
- name: Install dependencies
run: |
python setup.py install
# Used by ci_test.sh
pip install deepdiff
pip install solc-select
pip install "solc-select>=v1.0.0b1"
solc-select install all
solc-select use 0.5.1
- name: Set up nix
if: matrix.type == 'dapp'
uses: cachix/install-nix-action@v16
- name: Set up cachix
if: matrix.type == 'dapp'
uses: cachix/cachix-action@v10
with:
name: dapp
- name: Run Tests
env:
TEST_TYPE: ${{ matrix.type }}

@ -16,27 +16,31 @@ on:
jobs:
build:
name: Detectors tests
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-2022]
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: 3.6
- name: Install dependencies
run: |
python setup.py install
pip install deepdiff
pip install pytest
pip install solc-select
pip install "solc-select>=v1.0.0b1"
solc-select install all
solc-select use 0.7.3
- name: Test with pytest
run: |
pytest tests/test_detectors.py

@ -16,24 +16,29 @@ on:
jobs:
build:
name: Features tests
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-2022]
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: 3.6
- name: Install dependencies
run: |
python setup.py install
pip install deepdiff
pip install pytest
pip install solc-select
pip install "solc-select>=v1.0.0b1"
solc-select install all
solc-select use 0.8.0
@ -45,4 +50,3 @@ jobs:
run: |
pytest tests/test_features.py
pytest tests/test_constant_folding_unary.py

@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: 3.6
@ -36,7 +36,7 @@ jobs:
cp pyproject.toml .github/linters
- name: Lint everything else
uses: github/super-linter/slim@v4
uses: github/super-linter/slim@v4.9.2
if: always()
env:
# run linter on everything to catch preexisting problems

@ -16,32 +16,34 @@ on:
jobs:
build:
name: Parser tests
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-2022]
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: 3.6
- name: Install dependencies
run: |
python setup.py install
pip install deepdiff
pip install pytest
pip install "solc-select>=v1.0.0b1"
git clone https://github.com/crytic/solc-select.git
cd solc-select
git checkout 119dd05f58341811cb02b546f25269a7e8a10875
python setup.py install
- name: Install solc
run: |
solc-select install all
solc-select use 0.8.0
cd ..
- name: Test with pytest
run: |
pytest tests/test_ast_parsing.py

@ -1,3 +1,4 @@
---
name: pip-audit
on:
@ -14,7 +15,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install pip-audit

@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: 3.6
@ -36,7 +36,7 @@ jobs:
cp pyproject.toml .github/linters
- name: Pylint
uses: github/super-linter/slim@v4
uses: github/super-linter/slim@v4.9.2
if: always()
env:
# run linter on everything to catch preexisting problems

@ -37,7 +37,8 @@ To run them locally in the root dir of the repository:
- `pylint slither tests --rcfile pyproject.toml`
- `black . --config pyproject.toml`
We use pylint `2.12.2` black `21.10b0`.
We use pylint `2.13.4`, black `22.3.0`.
### Detectors tests
For each new detector, at least one regression tests must be present.

@ -8,23 +8,20 @@ cd test_dapp || exit 255
git config --global user.email "ci@trailofbits.com"
git config --global user.name "CI User"
curl https://nixos.org/nix/install | sh
# shellcheck disable=SC1090,SC1091
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
nix-env -iA nixpkgs.cachix
cachix use dapp
which nix-env || exit 255
git clone --recursive https://github.com/dapphub/dapptools "$HOME/.dapp/dapptools"
nix-env -f "$HOME/.dapp/dapptools" -iA dapp seth solc hevm ethsign
dapp init
slither .
slither . --detect external-function
if [ $? -eq 21 ]
# TODO: make more elaborate test
if [ $? -eq 4 ]
then
exit 0
fi
echo "Truffle test failed"
echo "Dapp test failed"
exit 255

@ -14,10 +14,10 @@ setup(
install_requires=[
"prettytable>=0.7.2",
"pysha3>=1.0.2",
"crytic-compile>=0.2.2",
# "crytic-compile",
# "crytic-compile>=0.2.2",
"crytic-compile",
],
# dependency_links=["git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile"],
dependency_links=["git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile"],
license="AGPL-3.0",
long_description=long_description,
entry_points={

@ -776,7 +776,7 @@ def main_impl(all_detector_classes, all_printer_classes):
if args.checklist:
output_results_to_markdown(results_detectors, args.checklist_limit)
# Dont print the number of result for printers
# Don't print the number of result for printers
if number_contracts == 0:
logger.warning(red("No contract was analyzed"))
if printer_classes:

@ -56,7 +56,12 @@ class CustomError(SourceMapping):
Contract and converted into address
:return: the solidity signature
"""
assert self._solidity_signature is not None
# Ideally this should be an assert
# But due to a logic limitation in the solc parsing (find_variable)
# We need to raise an error if the custom error sig was not yet built
# (set_solidity_sig was not called before find_variable)
if self._solidity_signature is None:
raise ValueError("Custom Error not yet built")
return self._solidity_signature
def set_solidity_sig(self) -> None:

@ -6,6 +6,7 @@ from slither.core.declarations.custom_error_top_level import CustomErrorTopLevel
from slither.core.declarations.enum_top_level import EnumTopLevel
from slither.core.declarations.function_top_level import FunctionTopLevel
from slither.core.declarations.structure_top_level import StructureTopLevel
from slither.core.solidity_types import TypeAlias
from slither.core.variables.top_level_variable import TopLevelVariable
from slither.slithir.variables import Constant
@ -44,6 +45,10 @@ class FileScope:
# local name -> original name (A -> B)
self.renaming: Dict[str, str] = {}
# User defined types
# Name -> type alias
self.user_defined_types: Dict[str, TypeAlias] = {}
def add_accesible_scopes(self) -> bool:
"""
Add information from accessible scopes. Return true if new information was obtained
@ -82,6 +87,9 @@ class FileScope:
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)
learn_something = True
return learn_something

@ -4,6 +4,7 @@
import json
import logging
import os
import posixpath
import re
from typing import Optional, Dict, List, Set, Union
@ -172,7 +173,7 @@ class SlitherCore(Context):
return False
mapping_elements_with_lines = (
(
os.path.normpath(elem["source_mapping"]["filename_absolute"]),
posixpath.normpath(elem["source_mapping"]["filename_absolute"]),
elem["source_mapping"]["lines"],
)
for elem in r["elements"]
@ -218,7 +219,7 @@ class SlitherCore(Context):
if "source_mapping" in elem
]
source_mapping_elements = map(
lambda x: os.path.normpath(x) if x else x, source_mapping_elements
lambda x: posixpath.normpath(x) if x else x, source_mapping_elements
)
matching = False

@ -5,3 +5,4 @@ from .mapping_type import MappingType
from .user_defined_type import UserDefinedType
from .type import Type
from .type_information import TypeInformation
from .type_alias import TypeAlias, TypeAliasTopLevel, TypeAliasContract

@ -43,8 +43,8 @@ Int = [
"int256",
]
Max_Int = {k: 2 ** (8 * i - 1) - 1 if i > 0 else 2 ** 255 - 1 for i, k in enumerate(Int)}
Min_Int = {k: -(2 ** (8 * i - 1)) if i > 0 else -(2 ** 255) for i, k in enumerate(Int)}
Max_Int = {k: 2 ** (8 * i - 1) - 1 if i > 0 else 2**255 - 1 for i, k in enumerate(Int)}
Min_Int = {k: -(2 ** (8 * i - 1)) if i > 0 else -(2**255) for i, k in enumerate(Int)}
Uint = [
"uint",
@ -82,7 +82,7 @@ Uint = [
"uint256",
]
Max_Uint = {k: 2 ** (8 * i) - 1 if i > 0 else 2 ** 256 - 1 for i, k in enumerate(Uint)}
Max_Uint = {k: 2 ** (8 * i) - 1 if i > 0 else 2**256 - 1 for i, k in enumerate(Uint)}
Min_Uint = {k: 0 for k in Uint}

@ -0,0 +1,41 @@
from typing import TYPE_CHECKING, Tuple
from slither.core.children.child_contract import ChildContract
from slither.core.declarations.top_level import TopLevel
from slither.core.solidity_types import Type
if TYPE_CHECKING:
from slither.core.declarations import Contract
from slither.core.scope.scope import FileScope
class TypeAlias(Type):
def __init__(self, underlying_type: Type, name: str):
super().__init__()
self.name = name
self.underlying_type = underlying_type
@property
def storage_size(self) -> Tuple[int, bool]:
return self.underlying_type.storage_size
def __hash__(self):
return hash(str(self))
class TypeAliasTopLevel(TypeAlias, TopLevel):
def __init__(self, underlying_type: Type, name: str, scope: "FileScope"):
super().__init__(underlying_type, name)
self.file_scope: "FileScope" = scope
def __str__(self):
return self.name
class TypeAliasContract(TypeAlias, ChildContract):
def __init__(self, underlying_type: Type, name: str, contract: "Contract"):
super().__init__(underlying_type, name)
self._contract: "Contract" = contract
def __str__(self):
return self.contract.name + "." + self.name

@ -29,7 +29,7 @@ class ConstantPragma(AbstractDetector):
versions = sorted(list(set(versions)))
if len(versions) > 1:
info = ["Different versions of Solidity is used:\n"]
info = ["Different versions of Solidity are used:\n"]
info += [f"\t- Version used: {[str(v) for v in versions]}\n"]
for p in pragma:

@ -87,7 +87,7 @@ contract Something {
if not intended_interface_is_subset_parent:
# Should not be a subset of an earlier determined intended_interface or derive from it
intended_interface_is_subset_intended = False
for intended_interface in intended_interfaces:
for intended_interface in list(intended_interfaces):
sigs_intended_interface = {
f.full_name for f in intended_interface.functions_entry_points
}

@ -89,14 +89,10 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if func.is_constructor:
continue
if not self.is_mixed_case(func.name):
if (
func.visibility
in [
if func.visibility in [
"internal",
"private",
]
and self.is_mixed_case_with_underscore(func.name)
):
] and self.is_mixed_case_with_underscore(func.name):
continue
if func.name.startswith("echidna_") or func.name.startswith("crytic_"):
continue

@ -1,7 +1,7 @@
import re
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
# pylint: disable=bidirectional-unicode
class RightToLeftOverride(AbstractDetector):
"""
Detect the usage of a Right-To-Left-Override (U+202E) character

@ -11,7 +11,7 @@ from slither.core.solidity_types.elementary_type import Int, Uint
def typeRange(t):
bits = int(t.split("int")[1])
if t in Uint:
return 0, (2 ** bits) - 1
return 0, (2**bits) - 1
if t in Int:
v = (2 ** (bits - 1)) - 1
return -v, v

@ -296,6 +296,24 @@ def _use_balance(slither: SlitherCore) -> Dict[str, List[str]]:
return ret
def _with_fallback(slither: SlitherCore) -> Set[str]:
ret: Set[str] = set()
for contract in slither.contracts:
for function in contract.functions_entry_points:
if function.is_fallback:
ret.add(contract.name)
return ret
def _with_receive(slither: SlitherCore) -> Set[str]:
ret: Set[str] = set()
for contract in slither.contracts:
for function in contract.functions_entry_points:
if function.is_receive:
ret.add(contract.name)
return ret
def _call_a_parameter(slither: SlitherCore) -> Dict[str, List[Dict]]:
"""
Detect the functions with external calls
@ -376,6 +394,10 @@ class Echidna(AbstractPrinter):
use_balance = _use_balance(self.slither)
with_fallback = list(_with_fallback(self.slither))
with_receive = list(_with_receive(self.slither))
d = {
"payable": payable,
"timestamp": timestamp,
@ -392,6 +414,8 @@ class Echidna(AbstractPrinter):
"call_a_parameter": call_parameters,
"use_balance": use_balance,
"solc_versions": [unit.solc_version for unit in self.slither.compilation_units],
"with_fallback": with_fallback,
"with_receive": with_receive,
}
self.info(json.dumps(d, indent=4))

@ -170,10 +170,10 @@ def _fits_under_integer(val: int, can_be_int: bool, can_be_uint) -> List[str]:
assert can_be_int | can_be_uint
while n <= 256:
if can_be_uint:
if val <= 2 ** n - 1:
if val <= 2**n - 1:
ret.append(f"uint{n}")
if can_be_int:
if val <= (2 ** n) / 2 - 1:
if val <= (2**n) / 2 - 1:
ret.append(f"int{n}")
n = n + 8
return ret
@ -196,7 +196,7 @@ def _fits_under_byte(val: Union[int, str]) -> List[str]:
return [f"bytes{size}"]
# val is a str
length = len(val.encode("utf-8"))
return [f"bytes{f}" for f in range(length, 33)]
return [f"bytes{f}" for f in range(length, 33)] + ["bytes"]
def _find_function_from_parameter(ir: Call, candidates: List[Function]) -> Optional[Function]:

@ -5,6 +5,7 @@ from slither.core.declarations import Modifier, Event, EnumContract, StructureCo
from slither.core.declarations.contract import Contract
from slither.core.declarations.custom_error_contract import CustomErrorContract
from slither.core.declarations.function_contract import FunctionContract
from slither.core.solidity_types import ElementaryType, TypeAliasContract
from slither.core.variables.state_variable import StateVariable
from slither.solc_parsing.declarations.caller_context import CallerContextExpression
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
@ -230,6 +231,7 @@ class ContractSolc(CallerContextExpression):
self.baseConstructorContractsCalled.append(referencedDeclaration)
def _parse_contract_items(self):
# pylint: disable=too-many-branches
if not self.get_children() in self._data: # empty contract
return
for item in self._data[self.get_children()]:
@ -253,10 +255,34 @@ class ContractSolc(CallerContextExpression):
self._usingForNotParsed.append(item)
elif item[self.get_key()] == "ErrorDefinition":
self._customErrorParsed.append(item)
elif item[self.get_key()] == "UserDefinedValueTypeDefinition":
self._parse_type_alias(item)
else:
raise ParsingError("Unknown contract item: " + item[self.get_key()])
return
def _parse_type_alias(self, item: Dict) -> None:
assert "name" in item
assert "underlyingType" in item
underlying_type = item["underlyingType"]
assert "nodeType" in underlying_type and underlying_type["nodeType"] == "ElementaryTypeName"
assert "name" in underlying_type
original_type = ElementaryType(underlying_type["name"])
# For user defined types defined at the contract level the lookup can be done
# Using the name or the canonical name
# For example during the type parsing the canonical name
# Note that Solidity allows shadowing of user defined types
# Between top level and contract definitions
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
def _parse_struct(self, struct: Dict):
st = StructureContract(self._contract.compilation_unit)

@ -18,6 +18,7 @@ from slither.core.solidity_types import (
ArrayType,
FunctionType,
MappingType,
TypeAlias,
)
from slither.core.variables.top_level_variable import TopLevelVariable
from slither.core.variables.variable import Variable
@ -136,9 +137,15 @@ def _find_top_level(
# For example, a top variable that use another top level variable
# IF more top level objects are added to Solidity, we have to be careful with the order of the lookup
# in this function
try:
for custom_error in scope.custom_errors:
if custom_error.solidity_signature == var_name:
return custom_error, False
except ValueError:
# This can happen as custom error sol signature might not have been built
# when find_variable was called
# TODO refactor find_variable to prevent this from happening
pass
return None, False
@ -207,9 +214,15 @@ def _find_in_contract(
# This is because when the dic is populated the underlying object is not yet parsed
# As a result, we need to iterate over all the custom errors here instead of using the dict
custom_errors = contract.custom_errors
try:
for custom_error in custom_errors:
if var_name == custom_error.solidity_signature:
return custom_error
except ValueError:
# This can happen as custom error sol signature might not have been built
# when find_variable was called
# TODO refactor find_variable to prevent this from happening
pass
# If the enum is refered as its name rather than its canonicalName
enums = {e.name: e for e in contract.enums}
@ -292,6 +305,7 @@ def find_variable(
Enum,
Structure,
CustomError,
TypeAlias,
],
bool,
]:
@ -337,6 +351,9 @@ 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

@ -15,6 +15,7 @@ from slither.core.declarations.import_directive import Import
from slither.core.declarations.pragma_directive import Pragma
from slither.core.declarations.structure_top_level import StructureTopLevel
from slither.core.scope.scope import FileScope
from slither.core.solidity_types import ElementaryType, TypeAliasTopLevel
from slither.core.variables.top_level_variable import TopLevelVariable
from slither.exceptions import SlitherException
from slither.solc_parsing.declarations.contract import ContractSolc
@ -298,6 +299,23 @@ class SlitherCompilationUnitSolc:
self._compilation_unit.custom_errors.append(custom_error)
self._custom_error_parser.append(custom_error_parser)
elif top_level_data[self.get_key()] == "UserDefinedValueTypeDefinition":
assert "name" in top_level_data
alias = top_level_data["name"]
assert "underlyingType" in top_level_data
underlying_type = top_level_data["underlyingType"]
assert (
"nodeType" in underlying_type
and underlying_type["nodeType"] == "ElementaryTypeName"
)
assert "name" in underlying_type
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)
scope.user_defined_types[alias] = user_defined_type
else:
raise SlitherException(f"Top level {top_level_data[self.get_key()]} not supported")

@ -6,6 +6,7 @@ from slither.core.declarations.custom_error_contract import CustomErrorContract
from slither.core.declarations.custom_error_top_level import CustomErrorTopLevel
from slither.core.declarations.function_contract import FunctionContract
from slither.core.expressions.literal import Literal
from slither.core.solidity_types import TypeAlias
from slither.core.solidity_types.array_type import ArrayType
from slither.core.solidity_types.elementary_type import (
ElementaryType,
@ -224,6 +225,7 @@ def parse_type(
sl: "SlitherCompilationUnit"
renaming: Dict[str, str]
user_defined_types: Dict[str, TypeAlias]
# Note: for convenicence top level functions use the same parser than function in contract
# but contract_parser is set to None
if isinstance(caller_context, SlitherCompilationUnitSolc) or (
@ -234,11 +236,13 @@ def parse_type(
sl = caller_context.compilation_unit
next_context = caller_context
renaming = {}
user_defined_types = {}
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
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]
@ -274,6 +278,7 @@ def parse_type(
functions = list(scope.functions)
renaming = scope.renaming
user_defined_types = scope.user_defined_types
elif isinstance(caller_context, (ContractSolc, FunctionSolc)):
if isinstance(caller_context, FunctionSolc):
underlying_func = caller_context.underlying_function
@ -302,6 +307,7 @@ def parse_type(
functions = contract.functions + contract.modifiers
renaming = scope.renaming
user_defined_types = scope.user_defined_types
else:
raise ParsingError(f"Incorrect caller context: {type(caller_context)}")
@ -315,6 +321,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]
return _find_from_type_name(
name,
functions,
@ -335,6 +343,8 @@ def parse_type(
name = t["typeDescriptions"]["typeString"]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
return user_defined_types[name]
return _find_from_type_name(
name,
functions,
@ -351,6 +361,8 @@ def parse_type(
name = t["attributes"][type_name_key]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
return user_defined_types[name]
return _find_from_type_name(
name,
functions,
@ -367,6 +379,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]
return _find_from_type_name(
name,
functions,

@ -279,6 +279,7 @@ class YulBlock(YulScope):
"""
# pylint: disable=redefined-slots-in-subclass
__slots__ = ["_entrypoint", "_parent_func", "_nodes", "node_scope"]
def __init__(

@ -23,6 +23,6 @@ def convert_string_to_int(val: Union[str, int]) -> int:
f"{base}e{expo} is too large to fit in any Solidity integer size"
)
return 0
return int(Decimal(base) * Decimal(10 ** expo))
return int(Decimal(base) * Decimal(10**expo))
return int(Decimal(val))

@ -43,7 +43,7 @@ class ConstantFolding(ExpressionVisitor):
left = get_val(expression.expression_left)
right = get_val(expression.expression_right)
if expression.type == BinaryOperationType.POWER:
set_val(expression, left ** right)
set_val(expression, left**right)
elif expression.type == BinaryOperationType.MULTIPLICATION:
set_val(expression, left * right)
elif expression.type == BinaryOperationType.DIVISION:

@ -1,10 +1,13 @@
import logging
from typing import List
from slither.core.declarations import (
Function,
SolidityVariable,
SolidityVariableComposed,
SolidityFunction,
Contract,
)
from slither.core.expressions import (
AssignmentOperationType,
@ -13,8 +16,9 @@ from slither.core.expressions import (
ElementaryTypeNameExpression,
CallExpression,
Identifier,
MemberAccess,
)
from slither.core.solidity_types import ArrayType, ElementaryType
from slither.core.solidity_types import ArrayType, ElementaryType, TypeAlias
from slither.core.solidity_types.type import Type
from slither.core.variables.local_variable_init_from_tuple import LocalVariableInitFromTuple
from slither.core.variables.variable import Variable
@ -33,6 +37,7 @@ from slither.slithir.operations import (
Unpack,
Return,
SolidityCall,
Operation,
)
from slither.slithir.tmp_operations.argument import Argument
from slither.slithir.tmp_operations.tmp_call import TmpCall
@ -59,6 +64,10 @@ def get(expression):
return val
def get_without_removing(expression):
return expression.context[key]
def set_val(expression, val):
expression.context[key] = val
@ -127,7 +136,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
self._expression = expression
self._node = node
self._result = []
self._result: List[Operation] = []
self._visit_expression(self.expression)
if node.type == NodeType.RETURN:
r = Return(get(self.expression))
@ -240,8 +249,13 @@ class ExpressionToSlithIR(ExpressionVisitor):
def _post_call_expression(
self, expression
): # pylint: disable=too-many-branches,too-many-statements
called = get(expression.called)
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals
assert isinstance(expression, CallExpression)
expression_called = expression.called
called = get(expression_called)
args = [get(a) for a in expression.arguments if a]
for arg in args:
arg_ = Argument(arg)
@ -259,9 +273,23 @@ class ExpressionToSlithIR(ExpressionVisitor):
internal_call.set_expression(expression)
self._result.append(internal_call)
set_val(expression, val)
else:
# User defined types
elif (
isinstance(called, TypeAlias)
and isinstance(expression_called, MemberAccess)
and expression_called.member_name in ["wrap", "unwrap"]
and len(args) == 1
):
val = TemporaryVariable(self._node)
var = TypeConversion(val, args[0], called)
var.set_expression(expression)
val.set_type(called)
self._result.append(var)
set_val(expression, val)
# yul things
if called.name == "caller()":
elif called.name == "caller()":
val = TemporaryVariable(self._node)
var = Assignment(val, SolidityVariableComposed("msg.sender"), "uint256")
self._result.append(var)
@ -297,6 +325,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
var = Assignment(val, SolidityVariableComposed("msg.value"), "uint256")
self._result.append(var)
set_val(expression, val)
else:
# If tuple
if expression.type_call.startswith("tuple(") and expression.type_call != "tuple()":
@ -413,6 +442,20 @@ class ExpressionToSlithIR(ExpressionVisitor):
set_val(expression, val)
return
if isinstance(expr, TypeAlias) and expression.member_name in ["wrap", "unwrap"]:
# The logic is be handled by _post_call_expression
set_val(expression, expr)
return
# Early lookup to detect user defined types from other contracts definitions
# 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 isinstance(expr, Contract):
if expression.member_name in expr.file_scope.user_defined_types:
set_val(expression, expr.file_scope.user_defined_types[expression.member_name])
return
val = ReferenceVariable(self._node)
member = Member(expr, Constant(expression.member_name), val)
member.set_expression(expression)

@ -0,0 +1,3 @@
# Always checkout test solidity code with lf endings
# autocrlf breaks file offsets in tests.
*.sol text eol=lf

@ -0,0 +1,17 @@
contract Log{
// Check that we can parse string/bytes
function f(bytes calldata) external{
}
function f(bytes4) external{
}
}
contract A{
Log l;
function test() internal{
l.f("TESTA");
}
}

@ -0,0 +1,9 @@
{
"Log": {
"f(bytes)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n",
"f(bytes4)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n"
},
"A": {
"test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n"
}
}

@ -0,0 +1,10 @@
{
"B": {
"u()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n",
"f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n}\n"
},
"D": {},
"C": {
"f(Left[])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n"
}
}

@ -0,0 +1,10 @@
{
"B": {
"u()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n",
"f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n}\n"
},
"D": {},
"C": {
"f(Left[])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n"
}
}

@ -0,0 +1,10 @@
{
"B": {
"u()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n",
"f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n}\n"
},
"D": {},
"C": {
"f(Left[])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n"
}
}

@ -0,0 +1,10 @@
{
"B": {
"u()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n",
"f()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n}\n"
},
"D": {},
"C": {
"f(Left[])": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n"
}
}

@ -0,0 +1,30 @@
type MyInt is uint;
contract B {
type MyInt is int;
function u() internal returns(int) {}
function f() public{
MyInt mi = MyInt.wrap(u());
}
}
function f(MyInt a) pure returns (MyInt b) {
b = MyInt(a);
}
contract D
{
B.MyInt x = B.MyInt.wrap(int(1));
}
contract C {
function f(Left[] memory a) internal returns(Left){
return a[0];
}
}
type Left is bytes2;
MyInt constant x = MyInt.wrap(20);

@ -55,10 +55,10 @@
}
}
],
"description": "Different versions of Solidity is used:\n\t- Version used: ['^0.4.24', '^0.4.25']\n\t- ^0.4.24 (tests/detectors/pragma/0.4.25/pragma.0.4.24.sol#1)\n\t- ^0.4.25 (tests/detectors/pragma/0.4.25/pragma.0.4.25.sol#1)\n",
"markdown": "Different versions of Solidity is used:\n\t- Version used: ['^0.4.24', '^0.4.25']\n\t- [^0.4.24](tests/detectors/pragma/0.4.25/pragma.0.4.24.sol#L1)\n\t- [^0.4.25](tests/detectors/pragma/0.4.25/pragma.0.4.25.sol#L1)\n",
"description": "Different versions of Solidity are used:\n\t- Version used: ['^0.4.24', '^0.4.25']\n\t- ^0.4.24 (tests/detectors/pragma/0.4.25/pragma.0.4.24.sol#1)\n\t- ^0.4.25 (tests/detectors/pragma/0.4.25/pragma.0.4.25.sol#1)\n",
"markdown": "Different versions of Solidity are used:\n\t- Version used: ['^0.4.24', '^0.4.25']\n\t- [^0.4.24](tests/detectors/pragma/0.4.25/pragma.0.4.24.sol#L1)\n\t- [^0.4.25](tests/detectors/pragma/0.4.25/pragma.0.4.25.sol#L1)\n",
"first_markdown_element": "tests/detectors/pragma/0.4.25/pragma.0.4.24.sol#L1",
"id": "4c6393f838949def61fef871f6771fee22a8783f10fcc084c78287fd2f1d60ed",
"id": "1b4bdffe0c7fc63e2a8d589f34ff29de46139cf79ff3b9cb13dee36502b8fbc6",
"check": "pragma",
"impact": "Informational",
"confidence": "High"

@ -55,10 +55,10 @@
}
}
],
"description": "Different versions of Solidity is used:\n\t- Version used: ['^0.5.15', '^0.5.16']\n\t- ^0.5.15 (tests/detectors/pragma/0.5.16/pragma.0.5.15.sol#1)\n\t- ^0.5.16 (tests/detectors/pragma/0.5.16/pragma.0.5.16.sol#1)\n",
"markdown": "Different versions of Solidity is used:\n\t- Version used: ['^0.5.15', '^0.5.16']\n\t- [^0.5.15](tests/detectors/pragma/0.5.16/pragma.0.5.15.sol#L1)\n\t- [^0.5.16](tests/detectors/pragma/0.5.16/pragma.0.5.16.sol#L1)\n",
"description": "Different versions of Solidity are used:\n\t- Version used: ['^0.5.15', '^0.5.16']\n\t- ^0.5.15 (tests/detectors/pragma/0.5.16/pragma.0.5.15.sol#1)\n\t- ^0.5.16 (tests/detectors/pragma/0.5.16/pragma.0.5.16.sol#1)\n",
"markdown": "Different versions of Solidity are used:\n\t- Version used: ['^0.5.15', '^0.5.16']\n\t- [^0.5.15](tests/detectors/pragma/0.5.16/pragma.0.5.15.sol#L1)\n\t- [^0.5.16](tests/detectors/pragma/0.5.16/pragma.0.5.16.sol#L1)\n",
"first_markdown_element": "tests/detectors/pragma/0.5.16/pragma.0.5.15.sol#L1",
"id": "fb131cf132ddad4e4550414541cc4b778b66e14a2a134874ad78197e54fbc020",
"id": "f3c6aef8c4d19f960e801fe9343d7cb4c290460cb7b4b14dca769269f0234b31",
"check": "pragma",
"impact": "Informational",
"confidence": "High"

@ -55,10 +55,10 @@
}
}
],
"description": "Different versions of Solidity is used:\n\t- Version used: ['^0.6.10', '^0.6.11']\n\t- ^0.6.10 (tests/detectors/pragma/0.6.11/pragma.0.6.10.sol#1)\n\t- ^0.6.11 (tests/detectors/pragma/0.6.11/pragma.0.6.11.sol#1)\n",
"markdown": "Different versions of Solidity is used:\n\t- Version used: ['^0.6.10', '^0.6.11']\n\t- [^0.6.10](tests/detectors/pragma/0.6.11/pragma.0.6.10.sol#L1)\n\t- [^0.6.11](tests/detectors/pragma/0.6.11/pragma.0.6.11.sol#L1)\n",
"description": "Different versions of Solidity are used:\n\t- Version used: ['^0.6.10', '^0.6.11']\n\t- ^0.6.10 (tests/detectors/pragma/0.6.11/pragma.0.6.10.sol#1)\n\t- ^0.6.11 (tests/detectors/pragma/0.6.11/pragma.0.6.11.sol#1)\n",
"markdown": "Different versions of Solidity are used:\n\t- Version used: ['^0.6.10', '^0.6.11']\n\t- [^0.6.10](tests/detectors/pragma/0.6.11/pragma.0.6.10.sol#L1)\n\t- [^0.6.11](tests/detectors/pragma/0.6.11/pragma.0.6.11.sol#L1)\n",
"first_markdown_element": "tests/detectors/pragma/0.6.11/pragma.0.6.10.sol#L1",
"id": "cca73553e40b6b9e04136a89efd83aea4283d56c8b3e49371384fc39e0a905e9",
"id": "05ad5ab9a596fc401c485ede8f164ebd0df3052e307bb036f70a64d47cbc2933",
"check": "pragma",
"impact": "Informational",
"confidence": "High"

@ -55,10 +55,10 @@
}
}
],
"description": "Different versions of Solidity is used:\n\t- Version used: ['^0.7.5', '^0.7.6']\n\t- ^0.7.5 (tests/detectors/pragma/0.7.6/pragma.0.7.5.sol#1)\n\t- ^0.7.6 (tests/detectors/pragma/0.7.6/pragma.0.7.6.sol#1)\n",
"markdown": "Different versions of Solidity is used:\n\t- Version used: ['^0.7.5', '^0.7.6']\n\t- [^0.7.5](tests/detectors/pragma/0.7.6/pragma.0.7.5.sol#L1)\n\t- [^0.7.6](tests/detectors/pragma/0.7.6/pragma.0.7.6.sol#L1)\n",
"description": "Different versions of Solidity are used:\n\t- Version used: ['^0.7.5', '^0.7.6']\n\t- ^0.7.5 (tests/detectors/pragma/0.7.6/pragma.0.7.5.sol#1)\n\t- ^0.7.6 (tests/detectors/pragma/0.7.6/pragma.0.7.6.sol#1)\n",
"markdown": "Different versions of Solidity are used:\n\t- Version used: ['^0.7.5', '^0.7.6']\n\t- [^0.7.5](tests/detectors/pragma/0.7.6/pragma.0.7.5.sol#L1)\n\t- [^0.7.6](tests/detectors/pragma/0.7.6/pragma.0.7.6.sol#L1)\n",
"first_markdown_element": "tests/detectors/pragma/0.7.6/pragma.0.7.5.sol#L1",
"id": "21aac87dfbfe74fd5fcee57ee3edcd75cb659d22b3845b2cc8508a8c44c87d1a",
"id": "bbddfc60b33090137a744cf0ebdfb2acbe88e3f353368626723e8bec7558f72f",
"check": "pragma",
"impact": "Informational",
"confidence": "High"

@ -401,6 +401,9 @@ ALL_TESTS = [
),
Test("custom_error_with_state_variable.sol", make_version(8, 4, 12)),
Test("complex_imports/import_aliases/test.sol", VERSIONS_08),
# 0.8.9 crashes on our testcase
Test("user_defined_types.sol", ["0.8.8"] + make_version(8, 10, 12)),
Test("bytes_call.sol", ["0.8.12"]),
]
# create the output folder if needed
try:

@ -1334,11 +1334,10 @@ def test_detector(test_item: Test):
for additional_file in test_item.additional_files:
additional_path = str(pathlib.Path(test_dir_path, additional_file).absolute())
results_as_string = results_as_string.replace(
additional_path, str(pathlib.Path(GENERIC_PATH))
)
results_as_string = results_as_string.replace(test_file_path, str(pathlib.Path(GENERIC_PATH)))
additional_path = additional_path.replace("\\", "\\\\")
results_as_string = results_as_string.replace(additional_path, GENERIC_PATH)
test_file_path = test_file_path.replace("\\", "\\\\")
results_as_string = results_as_string.replace(test_file_path, GENERIC_PATH)
results = json.loads(results_as_string)
diff = DeepDiff(results, expected_result, ignore_order=True, verbose_level=2)
@ -1378,13 +1377,13 @@ def _generate_test(test_item: Test, skip_existing=False):
results = sl.run_detectors()
results_as_string = json.dumps(results)
results_as_string = results_as_string.replace(test_file_path, str(pathlib.Path(GENERIC_PATH)))
test_file_path = test_file_path.replace("\\", "\\\\")
results_as_string = results_as_string.replace(test_file_path, GENERIC_PATH)
for additional_file in test_item.additional_files:
additional_path = str(pathlib.Path(test_dir_path, additional_file).absolute())
results_as_string = results_as_string.replace(
additional_path, str(pathlib.Path(GENERIC_PATH))
)
additional_path = additional_path.replace("\\", "\\\\")
results_as_string = results_as_string.replace(additional_path, GENERIC_PATH)
results = json.loads(results_as_string)
with open(expected_result_path, "w", encoding="utf8") as f:

@ -0,0 +1,934 @@
import os
import pathlib
from argparse import ArgumentTypeError
from collections import defaultdict
from contextlib import contextmanager
from inspect import getsourcefile
from tempfile import NamedTemporaryFile
from typing import Union, List, Optional
import pytest
from solc_select import solc_select
from solc_select.solc_select import valid_version as solc_valid_version
from slither import Slither
from slither.core.cfg.node import Node, NodeType
from slither.core.declarations import Function, Contract
from slither.core.variables.state_variable import StateVariable
from slither.slithir.operations import (
OperationWithLValue,
Phi,
Assignment,
HighLevelCall,
Return,
Operation,
Binary,
BinaryType,
InternalCall,
)
from slither.slithir.utils.ssa import is_used_later
from slither.slithir.variables import Constant, ReferenceVariable, LocalIRVariable, StateIRVariable
# Directory of currently executing script. Will be used as basis for temporary file names.
SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent
def valid_version(ver: str) -> bool:
"""Wrapper function to check if the solc-version is valid
The solc_select function raises and exception but for checks below,
only a bool is needed.
"""
try:
solc_valid_version(ver)
return True
except ArgumentTypeError:
return False
def have_ssa_if_ir(function: Function):
"""Verifies that all nodes in a function that have IR also have SSA IR"""
for n in function.nodes:
if n.irs:
assert n.irs_ssa
# pylint: disable=too-many-branches
def ssa_basic_properties(function: Function):
"""Verifies that basic properties of ssa holds
1. Every name is defined only once
2. A l-value is never index zero - there is always a zero-value available for each var
3. Every r-value is at least defined at some point
4. The number of ssa defs is >= the number of assignments to var
5. Function parameters SSA are stored in function.parameters_ssa
- if function parameter is_storage it refers to a fake variable
6. Function returns SSA are stored in function.returns_ssa
- if function return is_storage it refers to a fake variable
"""
ssa_lvalues = set()
ssa_rvalues = set()
lvalue_assignments = {}
for n in function.nodes:
for ir in n.irs:
if isinstance(ir, OperationWithLValue):
name = ir.lvalue.name
if name in lvalue_assignments:
lvalue_assignments[name] += 1
else:
lvalue_assignments[name] = 1
for ssa in n.irs_ssa:
if isinstance(ssa, OperationWithLValue):
# 1
assert ssa.lvalue not in ssa_lvalues
ssa_lvalues.add(ssa.lvalue)
# 2 (if Local/State Var)
if isinstance(ssa.lvalue, (StateIRVariable, LocalIRVariable)):
assert ssa.lvalue.index > 0
for rvalue in filter(
lambda x: not isinstance(x, (StateIRVariable, Constant)), ssa.read
):
ssa_rvalues.add(rvalue)
# 3
# Each var can have one non-defined value, the value initially held. Typically,
# var_0, i_0, state_0 or similar.
undef_vars = set()
for rvalue in ssa_rvalues:
if rvalue not in ssa_lvalues:
assert rvalue.non_ssa_version not in undef_vars
undef_vars.add(rvalue.non_ssa_version)
# 4
ssa_defs = defaultdict(int)
for v in ssa_lvalues:
ssa_defs[v.name] += 1
for (k, n) in lvalue_assignments.items():
assert ssa_defs[k] >= n
# Helper 5/6
def check_property_5_and_6(variables, ssavars):
for var in filter(lambda x: x.name, variables):
ssa_vars = [x for x in ssavars if x.non_ssa_version == var]
assert len(ssa_vars) == 1
ssa_var = ssa_vars[0]
assert var.is_storage == ssa_var.is_storage
if ssa_var.is_storage:
assert len(ssa_var.refers_to) == 1
assert ssa_var.refers_to[0].location == "reference_to_storage"
# 5
check_property_5_and_6(function.parameters, function.parameters_ssa)
# 6
check_property_5_and_6(function.returns, function.returns_ssa)
def ssa_phi_node_properties(f: Function):
"""Every phi-function should have as many args as predecessors
This does not apply if the phi-node refers to state variables,
they make use os special phi-nodes for tracking potential values
a state variable can have
"""
for node in f.nodes:
for ssa in node.irs_ssa:
if isinstance(ssa, Phi):
n = len(ssa.read)
if not isinstance(ssa.lvalue, StateIRVariable):
assert len(node.fathers) == n
# TODO (hbrodin): This should probably go into another file, not specific to SSA
def dominance_properties(f: Function):
"""Verifies properties related to dominators holds
1. Every node have an immediate dominator except entry_node which have none
2. From every node immediate dominator there is a path via its successors to the node
"""
def find_path(from_node: Node, to: Node) -> bool:
visited = set()
worklist = list(from_node.sons)
while worklist:
first, *worklist = worklist
if first == to:
return True
visited.add(first)
for successor in first.sons:
if successor not in visited:
worklist.append(successor)
return False
for node in f.nodes:
if node is f.entry_point:
assert node.immediate_dominator is None
else:
assert node.immediate_dominator is not None
assert find_path(node.immediate_dominator, node)
def phi_values_inserted(f: Function):
"""Verifies that phi-values are inserted at the right places
For every node that has a dominance frontier, any def (including
phi) should be a phi function in its dominance frontier
"""
def have_phi_for_var(node: Node, var):
"""Checks if a node has a phi-instruction for var
The ssa version would ideally be checked, but then
more data flow analysis would be needed, for cases
where a new def for var is introduced before reaching
DF
"""
non_ssa = var.non_ssa_version
for ssa in node.irs_ssa:
if isinstance(ssa, Phi):
if non_ssa in map(lambda ssa_var: ssa_var.non_ssa_version, ssa.read):
return True
return False
for node in filter(lambda n: n.dominance_frontier, f.nodes):
for df in node.dominance_frontier:
for ssa in node.irs_ssa:
if isinstance(ssa, OperationWithLValue):
if is_used_later(node, ssa.lvalue):
assert have_phi_for_var(df, ssa.lvalue)
@contextmanager
def select_solc_version(version: Optional[str]):
"""Selects solc version to use for running tests.
If no version is provided, latest is used."""
# If no solc_version selected just use the latest avail
if not version:
# This sorts the versions numerically
vers = sorted(
map(
lambda x: (int(x[0]), int(x[1]), int(x[2])),
map(lambda x: x.split(".", 3), solc_select.installed_versions()),
)
)
ver = list(vers)[-1]
version = ".".join(map(str, ver))
env = dict(os.environ)
env_restore = dict(env)
env["SOLC_VERSION"] = version
os.environ.clear()
os.environ.update(env)
yield version
os.environ.clear()
os.environ.update(env_restore)
@contextmanager
def slither_from_source(source_code: str, solc_version: Optional[str] = None):
"""Yields a Slither instance using source_code string and solc_version
Creates a temporary file and changes the solc-version temporary to solc_version.
"""
fname = ""
try:
with NamedTemporaryFile(dir=SCRIPT_DIR, mode="w", suffix=".sol", delete=False) as f:
fname = f.name
f.write(source_code)
with select_solc_version(solc_version):
yield Slither(fname)
finally:
pathlib.Path(fname).unlink()
def verify_properties_hold(source_code_or_slither: Union[str, Slither]):
"""Ensures that basic properties of SSA hold true"""
def verify_func(func: Function):
have_ssa_if_ir(func)
phi_values_inserted(func)
ssa_basic_properties(func)
ssa_phi_node_properties(func)
dominance_properties(func)
def verify(slither):
for cu in slither.compilation_units:
for func in cu.functions_and_modifiers:
_dump_function(func)
verify_func(func)
for contract in cu.contracts:
for f in contract.functions:
if f.is_constructor or f.is_constructor_variables:
_dump_function(f)
verify_func(f)
if isinstance(source_code_or_slither, Slither):
verify(source_code_or_slither)
else:
with slither_from_source(source_code_or_slither) as slither:
verify(slither)
def _dump_function(f: Function):
"""Helper function to print nodes/ssa ir for a function or modifier"""
print(f"---- {f.name} ----")
for n in f.nodes:
print(n)
for ir in n.irs_ssa:
print(f"\t{ir}")
print("")
def _dump_functions(c: Contract):
"""Helper function to print functions and modifiers of a contract"""
for f in c.functions_and_modifiers:
_dump_function(f)
def get_filtered_ssa(f: Union[Function, Node], flt) -> List[Operation]:
"""Returns a list of all ssanodes filtered by filter for all nodes in function f"""
if isinstance(f, Function):
return [ssanode for node in f.nodes for ssanode in node.irs_ssa if flt(ssanode)]
assert isinstance(f, Node)
return [ssanode for ssanode in f.irs_ssa if flt(ssanode)]
def get_ssa_of_type(f: Union[Function, Node], ssatype) -> List[Operation]:
"""Returns a list of all ssanodes of a specific type for all nodes in function f"""
return get_filtered_ssa(f, lambda ssanode: isinstance(ssanode, ssatype))
def test_multi_write():
contract = """
pragma solidity ^0.8.11;
contract Test {
function multi_write(uint val) external pure returns(uint) {
val = 1;
val = 2;
val = 3;
}
}"""
verify_properties_hold(contract)
def test_single_branch_phi():
contract = """
pragma solidity ^0.8.11;
contract Test {
function single_branch_phi(uint val) external pure returns(uint) {
if (val == 3) {
val = 9;
}
return val;
}
}
"""
verify_properties_hold(contract)
def test_basic_phi():
contract = """
pragma solidity ^0.8.11;
contract Test {
function basic_phi(uint val) external pure returns(uint) {
if (val == 3) {
val = 9;
} else {
val = 1;
}
return val;
}
}
"""
verify_properties_hold(contract)
def test_basic_loop_phi():
contract = """
pragma solidity ^0.8.11;
contract Test {
function basic_loop_phi(uint val) external pure returns(uint) {
for (uint i=0;i<128;i++) {
val = val + 1;
}
return val;
}
}
"""
verify_properties_hold(contract)
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_phi_propagation_loop():
contract = """
pragma solidity ^0.8.11;
contract Test {
function looping(uint v) external pure returns(uint) {
uint val = 0;
for (uint i=0;i<v;i++) {
if (val > i) {
val = i;
} else {
val = 3;
}
}
return val;
}
}
"""
verify_properties_hold(contract)
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_free_function_properties():
contract = """
pragma solidity ^0.8.11;
function free_looping(uint v) returns(uint) {
uint val = 0;
for (uint i=0;i<v;i++) {
if (val > i) {
val = i;
} else {
val = 3;
}
}
return val;
}
contract Test {}
"""
verify_properties_hold(contract)
def test_ssa_inter_transactional():
source = """
pragma solidity ^0.8.11;
contract A {
uint my_var_A;
uint my_var_B;
function direct_set(uint i) public {
my_var_A = i;
}
function direct_set_plus_one(uint i) public {
my_var_A = i + 1;
}
function indirect_set() public {
my_var_B = my_var_A;
}
}
"""
with slither_from_source(source) as slither:
c = slither.contracts[0]
variables = c.variables_as_dict
funcs = c.available_functions_as_dict()
direct_set = funcs["direct_set(uint256)"]
# Skip entry point and go straight to assignment ir
assign1 = direct_set.nodes[1].irs_ssa[0]
assert isinstance(assign1, Assignment)
assign2 = direct_set.nodes[1].irs_ssa[0]
assert isinstance(assign2, Assignment)
indirect_set = funcs["indirect_set()"]
phi = indirect_set.entry_point.irs_ssa[0]
assert isinstance(phi, Phi)
# phi rvalues come from 1, initial value of my_var_a and 2, assignment in direct_set
assert len(phi.rvalues) == 3
assert all(x.non_ssa_version == variables["my_var_A"] for x in phi.rvalues)
assert assign1.lvalue in phi.rvalues
assert assign2.lvalue in phi.rvalues
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_ssa_phi_callbacks():
source = """
pragma solidity ^0.8.11;
contract A {
uint my_var_A;
uint my_var_B;
function direct_set(uint i) public {
my_var_A = i;
}
function use_a() public {
// Expect a phi-node here
my_var_B = my_var_A;
B b = new B();
my_var_A = 3;
b.do_stuff();
// Expect a phi-node here
my_var_B = my_var_A;
}
}
contract B {
function do_stuff() public returns (uint) {
// This could be calling back into A
}
}
"""
with slither_from_source(source) as slither:
c = slither.get_contract_from_name("A")[0]
_dump_functions(c)
f = [x for x in c.functions if x.name == "use_a"][0]
var_a = [x for x in c.variables if x.name == "my_var_A"][0]
entry_phi = [
x
for x in f.entry_point.irs_ssa
if isinstance(x, Phi) and x.lvalue.non_ssa_version == var_a
][0]
# The four potential sources are:
# 1. initial value
# 2. my_var_A = i;
# 3. my_var_A = 3;
# 4. phi-value after call to b.do_stuff(), which could be reentrant.
assert len(entry_phi.rvalues) == 4
# Locate the first high-level call (should be b.do_stuff())
call_node = [x for y in f.nodes for x in y.irs_ssa if isinstance(x, HighLevelCall)][0]
n = call_node.node
# Get phi-node after call
after_call_phi = n.irs_ssa[n.irs_ssa.index(call_node) + 1]
# The two sources for this phi node is
# 1. my_var_A = i;
# 2. my_var_A = 3;
assert isinstance(after_call_phi, Phi)
assert len(after_call_phi.rvalues) == 2
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_storage_refers_to():
"""Test the storage aspects of the SSA IR
When declaring a var as being storage, start tracking what storage it refers_to.
When a phi-node is created, ensure refers_to is propagated to the phi-node.
Assignments also propagate refers_to.
Whenever a ReferenceVariable is the destination of an assignment (e.g. s.v = 10)
below, create additional versions of the variables it refers to record that a a
write was made. In the current implementation, this is referenced by phis.
"""
source = """
contract A{
struct St{
int v;
}
St state0;
St state1;
function f() public{
St storage s = state0;
if(true){
s = state1;
}
s.v = 10;
}
}
"""
with slither_from_source(source) as slither:
c = slither.contracts[0]
f = c.functions[0]
phinodes = get_ssa_of_type(f, Phi)
# Expect 2 in entrypoint (state0/state1 initial values), 1 at 'ENDIF' and two related to the
# ReferenceVariable write s.v = 10.
assert len(phinodes) == 5
# Assign s to state0, s to state1, s.v to 10
assigns = get_ssa_of_type(f, Assignment)
assert len(assigns) == 3
# The IR variables have is_storage
assert all(x.lvalue.is_storage for x in assigns if isinstance(x, LocalIRVariable))
# s.v ReferenceVariable points to one of the phi vars...
ref0 = [x.lvalue for x in assigns if isinstance(x.lvalue, ReferenceVariable)][0]
sphis = [x for x in phinodes if x.lvalue == ref0.points_to]
assert len(sphis) == 1
sphi = sphis[0]
# ...and that phi refers to the two entry phi-values
entryphi = [x for x in phinodes if x.lvalue in sphi.lvalue.refers_to]
assert len(entryphi) == 2
# The remaining two phis are the ones recording that write through ReferenceVariable occured
for ephi in entryphi:
phinodes.remove(ephi)
phinodes.remove(sphi)
assert len(phinodes) == 2
# And they are recorded in one of the entry phis
assert phinodes[0].lvalue in entryphi[0].rvalues or entryphi[1].rvalues
assert phinodes[1].lvalue in entryphi[0].rvalues or entryphi[1].rvalues
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
@pytest.mark.skipif(
not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform"
)
def test_initial_version_exists_for_locals():
"""
In solidity you can write statements such as
uint a = a + 1, this test ensures that can be handled for local variables.
"""
src = """
contract C {
function func() internal {
uint a = a + 1;
}
}
"""
with slither_from_source(src, "0.4.0") as slither:
verify_properties_hold(slither)
c = slither.contracts[0]
f = c.functions[0]
addition = get_ssa_of_type(f, Binary)[0]
assert addition.type == BinaryType.ADDITION
assert isinstance(addition.variable_right, Constant)
a_0 = addition.variable_left
assert a_0.index == 0
assert a_0.name == "a"
assignment = get_ssa_of_type(f, Assignment)[0]
a_1 = assignment.lvalue
assert a_1.index == 1
assert a_1.name == "a"
assert assignment.rvalue == addition.lvalue
assert a_0.non_ssa_version == a_1.non_ssa_version
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
@pytest.mark.skipif(
not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform"
)
def test_initial_version_exists_for_state_variables():
"""
In solidity you can write statements such as
uint a = a + 1, this test ensures that can be handled for state variables.
"""
src = """
contract C {
uint a = a + 1;
}
"""
with slither_from_source(src, "0.4.0") as slither:
verify_properties_hold(slither)
c = slither.contracts[0]
f = c.functions[0] # There will be one artificial ctor function for the state vars
addition = get_ssa_of_type(f, Binary)[0]
assert addition.type == BinaryType.ADDITION
assert isinstance(addition.variable_right, Constant)
a_0 = addition.variable_left
assert isinstance(a_0, StateIRVariable)
assert a_0.name == "a"
assignment = get_ssa_of_type(f, Assignment)[0]
a_1 = assignment.lvalue
assert isinstance(a_1, StateIRVariable)
assert a_1.index == a_0.index + 1
assert a_1.name == "a"
assert assignment.rvalue == addition.lvalue
assert a_0.non_ssa_version == a_1.non_ssa_version
assert isinstance(a_0.non_ssa_version, StateVariable)
# No conditional/other function interaction so no phis
assert len(get_ssa_of_type(f, Phi)) == 0
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_initial_version_exists_for_state_variables_function_assign():
"""
In solidity you can write statements such as
uint a = a + 1, this test ensures that can be handled for local variables.
"""
# TODO (hbrodin): Could be a detector that a is not used in f
src = """
contract C {
uint a = f();
function f() internal returns(uint) {
return a;
}
}
"""
with slither_from_source(src) as slither:
verify_properties_hold(slither)
c = slither.contracts[0]
f, ctor = c.functions
if f.is_constructor_variables:
f, ctor = ctor, f
# ctor should have a single call to f that assigns to a
# temporary variable, that is then assigned to a
call = get_ssa_of_type(ctor, InternalCall)[0]
assert call.function == f
assign = get_ssa_of_type(ctor, Assignment)[0]
assert assign.rvalue == call.lvalue
assert isinstance(assign.lvalue, StateIRVariable)
assert assign.lvalue.name == "a"
# f should have a phi node on entry of a0, a1 and should return
# a2
phi = get_ssa_of_type(f, Phi)[0]
assert len(phi.rvalues) == 2
assert assign.lvalue in phi.rvalues
@pytest.mark.skipif(
not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform"
)
def test_return_local_before_assign():
src = """
// this require solidity < 0.5
// a variable can be returned before declared. Ensure it can be
// handled by Slither.
contract A {
function local(bool my_bool) internal returns(uint){
if(my_bool){
return a_local;
}
uint a_local = 10;
}
}
"""
with slither_from_source(src, "0.4.0") as slither:
f = slither.contracts[0].functions[0]
ret = get_ssa_of_type(f, Return)[0]
assert len(ret.values) == 1
assert ret.values[0].index == 0
assign = get_ssa_of_type(f, Assignment)[0]
assert assign.lvalue.index == 1
assert assign.lvalue.non_ssa_version == ret.values[0].non_ssa_version
@pytest.mark.skipif(
not valid_version("0.5.0"), reason="Solidity version 0.5.0 not available on this platform"
)
def test_shadow_local():
src = """
contract A {
// this require solidity 0.5
function shadowing_local() internal{
uint local = 0;
{
uint local = 1;
{
uint local = 2;
}
}
}
}
"""
with slither_from_source(src, "0.5.0") as slither:
_dump_functions(slither.contracts[0])
f = slither.contracts[0].functions[0]
# Ensure all assignments are to a variable of index 1
# not using the same IR var.
assert all(map(lambda x: x.lvalue.index == 1, get_ssa_of_type(f, Assignment)))
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_multiple_named_args_returns():
"""Verifies that named arguments and return values have correct versions
Each arg/ret have an initial version, version 0, and is written once and should
then have version 1.
"""
src = """
contract A {
function multi(uint arg1, uint arg2) internal returns (uint ret1, uint ret2) {
arg1 = arg1 + 1;
arg2 = arg2 + 2;
ret1 = arg1 + 3;
ret2 = arg2 + 4;
}
}"""
with slither_from_source(src) as slither:
verify_properties_hold(slither)
f = slither.contracts[0].functions[0]
# Ensure all LocalIRVariables (not TemporaryVariables) have index 1
assert all(
map(
lambda x: x.lvalue.index == 1 or not isinstance(x.lvalue, LocalIRVariable),
get_ssa_of_type(f, OperationWithLValue),
)
)
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_issue_468():
"""
Ensure issue 468 is corrected as per
https://github.com/crytic/slither/issues/468#issuecomment-620974151
The one difference is that we allow the phi-function at entry of f to
hold exit state which contains init state and state from branch, which
is a bit redundant. This could be further simplified.
"""
source = """
contract State {
int state = 0;
function f(int a) public returns (int) {
// phi-node here for state
if (a < 1) {
state += 1;
}
// phi-node here for state
return state;
}
}
"""
with slither_from_source(source) as slither:
c = slither.get_contract_from_name("State")[0]
f = [x for x in c.functions if x.name == "f"][0]
# Check that there is an entry point phi values for each later value
# plus one additional which is the initial value
entry_ssa = f.entry_point.irs_ssa
assert len(entry_ssa) == 1
phi_entry = entry_ssa[0]
assert isinstance(phi_entry, Phi)
# Find the second phi function
endif_node = [x for x in f.nodes if x.type == NodeType.ENDIF][0]
assert len(endif_node.irs_ssa) == 1
phi_endif = endif_node.irs_ssa[0]
assert isinstance(phi_endif, Phi)
# Ensure second phi-function contains init-phi and one additional
assert len(phi_endif.rvalues) == 2
assert phi_entry.lvalue in phi_endif.rvalues
# Find return-statement and ensure it returns the phi_endif
return_node = [x for x in f.nodes if x.type == NodeType.RETURN][0]
assert len(return_node.irs_ssa) == 1
ret = return_node.irs_ssa[0]
assert len(ret.values) == 1
assert phi_endif.lvalue in ret.values
# Ensure that the phi_endif (which is the end-state for function as well) is in the entry_phi
assert phi_endif.lvalue in phi_entry.rvalues
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_issue_434():
source = """
contract Contract {
int public a;
function f() public {
g();
a += 1;
}
function e() public {
a -= 1;
}
function g() public {
e();
}
}
"""
with slither_from_source(source) as slither:
c = slither.get_contract_from_name("Contract")[0]
e = [x for x in c.functions if x.name == "e"][0]
f = [x for x in c.functions if x.name == "f"][0]
g = [x for x in c.functions if x.name == "g"][0]
# Ensure there is a phi-node at the beginning of f and e
phi_entry_e = get_ssa_of_type(e.entry_point, Phi)[0]
phi_entry_f = get_ssa_of_type(f.entry_point, Phi)[0]
# But not at g
assert len(get_ssa_of_type(g, Phi)) == 0
# Ensure that the final states of f and e are in the entry-states
add_f = get_filtered_ssa(
f, lambda x: isinstance(x, Binary) and x.type == BinaryType.ADDITION
)[0]
sub_e = get_filtered_ssa(
e, lambda x: isinstance(x, Binary) and x.type == BinaryType.SUBTRACTION
)[0]
assert add_f.lvalue in phi_entry_f.rvalues
assert add_f.lvalue in phi_entry_e.rvalues
assert sub_e.lvalue in phi_entry_f.rvalues
assert sub_e.lvalue in phi_entry_e.rvalues
# Ensure there is a phi-node after call to g
call = get_ssa_of_type(f, InternalCall)[0]
idx = call.node.irs_ssa.index(call)
aftercall_phi = call.node.irs_ssa[idx + 1]
assert isinstance(aftercall_phi, Phi)
# Ensure that phi node ^ is used in the addition afterwards
assert aftercall_phi.lvalue in (add_f.variable_left, add_f.variable_right)
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_issue_473():
source = """
contract Contract {
function f() public returns (int) {
int a = 1;
if (a > 0) {
a = 2;
}
if (a == 3) {
a = 6;
}
return a;
}
}
"""
with slither_from_source(source) as slither:
c = slither.get_contract_from_name("Contract")[0]
f = c.functions[0]
phis = get_ssa_of_type(f, Phi)
return_value = get_ssa_of_type(f, Return)[0]
# There shall be two phi functions
assert len(phis) == 2
first_phi = phis[0]
second_phi = phis[1]
# The second phi is the one being returned, if it's the first swap them (iteration order)
if first_phi.lvalue in return_value.values:
first_phi, second_phi = second_phi, first_phi
# First phi is for [a=1 or a=2]
assert len(first_phi.rvalues) == 2
# second is for [a=6 or first phi]
assert first_phi.lvalue in second_phi.rvalues
assert len(second_phi.rvalues) == 2
# return is for second phi
assert len(return_value.values) == 1
assert second_phi.lvalue in return_value.values
Loading…
Cancel
Save