From 0d3115fda08baf9bb559ef7f317e6efa92d21bb2 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 28 Mar 2023 11:54:16 -0500 Subject: [PATCH 01/13] run tests in parallel locally with makefile --- .github/workflows/test.yml | 2 - Makefile | 87 + setup.py | 2 + tests/conftest.py | 27 + tests/e2e/compilation/test_resolution.py | 12 +- tests/tools/read-storage/test_read_storage.py | 6 +- tests/unit/core/test_arithmetic.py | 6 +- tests/unit/core/test_code_comments.py | 18 +- tests/unit/core/test_constant_folding.py | 15 +- tests/unit/core/test_contract_declaration.py | 17 +- tests/unit/core/test_function_declaration.py | 24 +- tests/unit/core/test_source_mapping.py | 29 +- tests/unit/core/test_storage_layout.py | 10 +- tests/unit/core/test_using_for.py | 36 +- tests/unit/slithir/test_operation_reads.py | 6 +- tests/unit/slithir/test_ssa_generation.py | 2161 ++++++++--------- .../unit/slithir/test_ternary_expressions.py | 6 +- tests/unit/utils/test_code_generation.py | 6 +- tests/unit/utils/test_functions_ids.py | 6 +- tests/unit/utils/test_type_helpers.py | 6 +- 20 files changed, 1287 insertions(+), 1195 deletions(-) create mode 100644 Makefile create mode 100644 tests/conftest.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44bba58788..a003eb168c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,8 +37,6 @@ jobs: - name: Install dependencies run: | pip install ".[test]" - solc-select install 0.8.0 - solc-select use 0.8.0 - name: Setup node uses: actions/setup-node@v3 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..cc102c9586 --- /dev/null +++ b/Makefile @@ -0,0 +1,87 @@ +SHELL := /bin/bash + +PY_MODULE := slither + +ALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py') \ + $(shell find test -name '*.py') + +# Optionally overriden by the user, if they're using a virtual environment manager. +VENV ?= env + +# On Windows, venv scripts/shims are under `Scripts` instead of `bin`. +VENV_BIN := $(VENV)/bin +ifeq ($(OS),Windows_NT) + VENV_BIN := $(VENV)/Scripts +endif + +# Optionally overridden by the user in the `release` target. +BUMP_ARGS := + +# Optionally overridden by the user in the `test` target. +TESTS := + +# Optionally overridden by the user/CI, to limit the installation to a specific +# subset of development dependencies. +SLITHER_EXTRA := dev + +# If the user selects a specific test pattern to run, set `pytest` to fail fast +# and only run tests that match the pattern. +# Otherwise, run all tests and enable coverage assertions, since we expect +# complete test coverage. +ifneq ($(TESTS),) + TEST_ARGS := -x -k $(TESTS) + COV_ARGS := +else + TEST_ARGS := -n auto + COV_ARGS := --cov-append # --fail-under 100 +endif + +.PHONY: all +all: + @echo "Run my targets individually!" + +.PHONY: dev +dev: $(VENV)/pyvenv.cfg + +.PHONY: run +run: $(VENV)/pyvenv.cfg + @. $(VENV_BIN)/activate && slither $(ARGS) + +$(VENV)/pyvenv.cfg: pyproject.toml + # Create our Python 3 virtual environment + python3 -m venv env + $(VENV_BIN)/python -m pip install --upgrade pip + $(VENV_BIN)/python -m pip install -e .[$(SLITHER_EXTRA)] + +.PHONY: lint +lint: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + black --check $(ALL_PY_SRCS) && \ + pylint $(ALL_PY_SRCS) + # ruff $(ALL_PY_SRCS) && \ + # mypy $(PY_MODULE) && + +.PHONY: reformat +reformat: + . $(VENV_BIN)/activate && \ + black $(PY_MODULE) + +.PHONY: test tests +test tests: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + pytest --cov=$(PY_MODULE) $(T) $(TEST_ARGS) && \ + python -m coverage report -m $(COV_ARGS) + +.PHONY: doc +doc: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + PDOC_ALLOW_EXEC=1 pdoc -o html slither '!slither.tools' + +.PHONY: package +package: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + python3 -m build + +.PHONY: edit +edit: + $(EDITOR) $(ALL_PY_SRCS) \ No newline at end of file diff --git a/setup.py b/setup.py index 1052098514..4ec357ac02 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ # "crytic-compile>=0.3.0", "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", "web3>=6.0.0", + "solc-select@git+https://github.com/crytic/solc-select.git@query-artifact-path#egg=solc-select", ], extras_require={ "lint": [ @@ -31,6 +32,7 @@ "deepdiff", "numpy", "coverage[toml]", + "filelock", ], "doc": [ "pdoc", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..bf15b27d11 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,27 @@ +import pytest +from filelock import FileLock +from solc_select import solc_select + +@pytest.fixture(scope="session") +def solc_versions_installed(): + """List of solc versions available in the test environment.""" + return [] + +@pytest.fixture(scope="session", autouse=True) +def register_solc_versions_installed(solc_versions_installed): + solc_versions_installed.extend(solc_select.installed_versions()) + +@pytest.fixture(scope="session") +def use_solc_version(request, solc_versions_installed): + def _use_solc_version(version): + print(version) + if version not in solc_versions_installed: + print("Installing solc version", version) + solc_select.install_artifacts([version]) + artifact_path = solc_select.artifact_path(version) + lock = FileLock(artifact_path) + try: + yield artifact_path + finally: + lock.release() + return _use_solc_version diff --git a/tests/e2e/compilation/test_resolution.py b/tests/e2e/compilation/test_resolution.py index 4b50b07374..3444af2e90 100644 --- a/tests/e2e/compilation/test_resolution.py +++ b/tests/e2e/compilation/test_resolution.py @@ -24,8 +24,8 @@ def test_node_modules() -> None: _run_all_detectors(slither) -def test_contract_name_collisions() -> None: - solc_select.switch_global_version("0.8.0", always_install=True) +def test_contract_name_collision(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.0")) standard_json = SolcStandardJson() standard_json.add_source_file( Path(TEST_DATA_DIR, "test_contract_name_collisions", "a.sol").as_posix() @@ -34,13 +34,13 @@ def test_contract_name_collisions() -> None: Path(TEST_DATA_DIR, "test_contract_name_collisions", "b.sol").as_posix() ) - compilation = CryticCompile(standard_json) + compilation = CryticCompile(standard_json, solc=solc_path) slither = Slither(compilation) _run_all_detectors(slither) -def test_cycle() -> None: - solc_select.switch_global_version("0.8.0", always_install=True) - slither = Slither(Path(TEST_DATA_DIR, "test_cyclic_import", "a.sol").as_posix()) +def test_cycle(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.0")) + slither = Slither(Path(TEST_DATA_DIR, "test_cyclic_import", "a.sol").as_posix(), solc=solc_path) _run_all_detectors(slither) diff --git a/tests/tools/read-storage/test_read_storage.py b/tests/tools/read-storage/test_read_storage.py index 38d909bf88..3b83df8558 100644 --- a/tests/tools/read-storage/test_read_storage.py +++ b/tests/tools/read-storage/test_read_storage.py @@ -90,7 +90,9 @@ def deploy_contract(w3, ganache, contract_bin, contract_abi) -> Contract: # pylint: disable=too-many-locals @pytest.mark.usefixtures("web3", "ganache") -def test_read_storage(web3, ganache) -> None: +def test_read_storage(web3, ganache, use_solc_version) -> None: + solc_path = next(use_solc_version(version="0.8.10")) + assert web3.is_connected() bin_path = Path(TEST_DATA_DIR, "StorageLayout.bin").as_posix() abi_path = Path(TEST_DATA_DIR, "StorageLayout.abi").as_posix() @@ -100,7 +102,7 @@ def test_read_storage(web3, ganache) -> None: contract.functions.store().transact({"from": ganache.eth_address}) address = contract.address - sl = Slither(Path(TEST_DATA_DIR, "storage_layout-0.8.10.sol").as_posix()) + sl = Slither(Path(TEST_DATA_DIR, "storage_layout-0.8.10.sol").as_posix(), solc=solc_path) contracts = sl.contracts srs = SlitherReadStorage(contracts, 100) diff --git a/tests/unit/core/test_arithmetic.py b/tests/unit/core/test_arithmetic.py index 621ff0f940..6e7843ea0d 100644 --- a/tests/unit/core/test_arithmetic.py +++ b/tests/unit/core/test_arithmetic.py @@ -8,9 +8,9 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" / "arithmetic_usage" -def test_arithmetic_usage() -> None: - solc_select.switch_global_version("0.8.15", always_install=True) - slither = Slither(Path(TEST_DATA_DIR, "test.sol").as_posix()) +def test_arithmetic_usage(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) + slither = Slither(Path(TEST_DATA_DIR, "test.sol").as_posix(), solc=solc_path) assert { f.source_mapping.content_hash for f in unchecked_arithemtic_usage(slither.contracts[0]) diff --git a/tests/unit/core/test_code_comments.py b/tests/unit/core/test_code_comments.py index 01b9ff336d..4fbbae658b 100644 --- a/tests/unit/core/test_code_comments.py +++ b/tests/unit/core/test_code_comments.py @@ -8,9 +8,9 @@ CUSTOM_COMMENTS_TEST_DATA_DIR = Path(TEST_DATA_DIR, "custom_comments") -def test_upgradeable_comments() -> None: - solc_select.switch_global_version("0.8.10", always_install=True) - slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "upgrade.sol").as_posix()) +def test_upgradeable_comments(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.10")) + slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "upgrade.sol").as_posix(), solc=solc_path) compilation_unit = slither.compilation_units[0] proxy = compilation_unit.get_contract_from_name("Proxy")[0] @@ -27,11 +27,11 @@ def test_upgradeable_comments() -> None: assert v1.upgradeable_version == "version_1" -def test_contract_comments() -> None: +def test_contract_comments(use_solc_version) -> None: comments = " @title Test Contract\n @dev Test comment" - solc_select.switch_global_version("0.8.10", always_install=True) - slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix()) + solc_path = next(use_solc_version("0.8.10")) + slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix(), solc=solc_path) compilation_unit = slither.compilation_units[0] contract = compilation_unit.get_contract_from_name("A")[0] @@ -40,8 +40,8 @@ def test_contract_comments() -> None: # Old solc versions have a different parsing of comments # the initial space (after *) is also not kept on every line comments = "@title Test Contract\n@dev Test comment" - solc_select.switch_global_version("0.5.16", always_install=True) - slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix()) + solc_path = next(use_solc_version("0.5.16")) + slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix(), solc=solc_path) compilation_unit = slither.compilation_units[0] contract = compilation_unit.get_contract_from_name("A")[0] @@ -49,10 +49,10 @@ def test_contract_comments() -> None: # Test with legacy AST comments = "@title Test Contract\n@dev Test comment" - solc_select.switch_global_version("0.5.16", always_install=True) slither = Slither( Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix(), solc_force_legacy_json=True, + solc=solc_path, ) compilation_unit = slither.compilation_units[0] contract = compilation_unit.get_contract_from_name("A")[0] diff --git a/tests/unit/core/test_constant_folding.py b/tests/unit/core/test_constant_folding.py index eb40a43c0b..d01b35dc0e 100644 --- a/tests/unit/core/test_constant_folding.py +++ b/tests/unit/core/test_constant_folding.py @@ -6,13 +6,15 @@ CONSTANT_FOLDING_TEST_ROOT = Path(TEST_DATA_DIR, "constant_folding") -def test_constant_folding_unary(): +def test_constant_folding_unary(use_solc_version): + solc_path = next(use_solc_version("0.8.0")) file = Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_unary.sol").as_posix() - Slither(file) + Slither(file, solc=solc_path) -def test_constant_folding_rational(): - s = Slither(Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_rational.sol").as_posix()) +def test_constant_folding_rational(use_solc_version): + solc_path = next(use_solc_version("0.8.0")) + s = Slither(Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_rational.sol").as_posix(), solc=solc_path) contract = s.get_contract_from_name("C")[0] variable_a = contract.get_state_variable_from_name("a") @@ -50,8 +52,9 @@ def test_constant_folding_rational(): assert str(ConstantFolding(variable_g.expression, "int64").result()) == "-7" -def test_constant_folding_binary_expressions(): - sl = Slither(Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_binop.sol").as_posix()) +def test_constant_folding_binary_expressions(use_solc_version): + solc_path = next(use_solc_version("0.8.0")) + sl = Slither(Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_binop.sol").as_posix(), solc=solc_path) contract = sl.get_contract_from_name("BinOp")[0] variable_a = contract.get_state_variable_from_name("a") diff --git a/tests/unit/core/test_contract_declaration.py b/tests/unit/core/test_contract_declaration.py index db9a141f59..3c1e7175e0 100644 --- a/tests/unit/core/test_contract_declaration.py +++ b/tests/unit/core/test_contract_declaration.py @@ -9,25 +9,26 @@ CONTRACT_DECL_TEST_ROOT = Path(TEST_DATA_DIR, "contract_declaration") -def test_abstract_contract() -> None: - solc_select.switch_global_version("0.8.0", always_install=True) - slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "abstract.sol").as_posix()) +def test_abstract_contract(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.0")) + slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "abstract.sol").as_posix(), solc=solc_path) assert not slither.contracts[0].is_fully_implemented - solc_select.switch_global_version("0.5.0", always_install=True) - slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix()) + solc_path = next(use_solc_version("0.5.0")) + slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix(), solc=solc_path) assert not slither.contracts[0].is_fully_implemented slither = Slither( Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix(), solc_force_legacy_json=True, + solc=solc_path ) assert not slither.contracts[0].is_fully_implemented -def test_private_variable() -> None: - solc_select.switch_global_version("0.8.15", always_install=True) - slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "private_variable.sol").as_posix()) +def test_private_variable(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) + slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "private_variable.sol").as_posix(), solc=solc_path) contract_c = slither.get_contract_from_name("C")[0] f = contract_c.functions[0] var_read = f.variables_read[0] diff --git a/tests/unit/core/test_function_declaration.py b/tests/unit/core/test_function_declaration.py index 6f7aa23e7a..4faa9d919c 100644 --- a/tests/unit/core/test_function_declaration.py +++ b/tests/unit/core/test_function_declaration.py @@ -15,11 +15,11 @@ FUNC_DELC_TEST_ROOT = Path(TEST_DATA_DIR, "function_declaration") -def test_functions(): +def test_functions(use_solc_version): # pylint: disable=too-many-statements - solc_select.switch_global_version("0.6.12", always_install=True) + solc_path = next(use_solc_version("0.6.12")) file = Path(FUNC_DELC_TEST_ROOT, "test_function.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) functions = slither.get_contract_from_name("TestFunction")[0].available_functions_as_dict() f = functions["external_payable(uint256)"] @@ -248,10 +248,10 @@ def test_functions(): assert f.return_type[0] == ElementaryType("bool") -def test_function_can_send_eth(): - solc_select.switch_global_version("0.6.12", always_install=True) +def test_function_can_send_eth(use_solc_version): + solc_path = next(use_solc_version("0.6.12")) file = Path(FUNC_DELC_TEST_ROOT, "test_function.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) compilation_unit = slither.compilation_units[0] functions = compilation_unit.get_contract_from_name("TestFunctionCanSendEth")[ 0 @@ -273,10 +273,10 @@ def test_function_can_send_eth(): assert functions["highlevel_call_via_external()"].can_send_eth() is False -def test_reentrant(): - solc_select.switch_global_version("0.8.10", always_install=True) +def test_reentrant(use_solc_version): + solc_path = next(use_solc_version("0.8.10")) file = Path(FUNC_DELC_TEST_ROOT, "test_function_reentrant.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) compilation_unit = slither.compilation_units[0] functions = compilation_unit.get_contract_from_name("TestReentrant")[ 0 @@ -290,10 +290,10 @@ def test_reentrant(): assert functions["internal_and_reentrant()"].is_reentrant -def test_public_variable() -> None: - solc_select.switch_global_version("0.6.12", always_install=True) +def test_public_variable(use_solc_version) -> None: + solc_path = next(use_solc_version("0.6.12")) file = Path(FUNC_DELC_TEST_ROOT, "test_function.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) contracts = slither.get_contract_from_name("TestFunction") assert len(contracts) == 1 contract = contracts[0] diff --git a/tests/unit/core/test_source_mapping.py b/tests/unit/core/test_source_mapping.py index 745d391d94..2dd8c24359 100644 --- a/tests/unit/core/test_source_mapping.py +++ b/tests/unit/core/test_source_mapping.py @@ -8,14 +8,13 @@ SRC_MAPPING_TEST_ROOT = Path(TEST_DATA_DIR, "src_mapping") -def test_source_mapping(): - solc_select.switch_global_version("0.6.12", always_install=True) +def test_source_mapping(use_solc_version): + solc_path = next(use_solc_version("0.6.12")) file = Path(SRC_MAPPING_TEST_ROOT, "inheritance.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) # Check if A.f() is at the offset 27 functions = slither.offset_to_objects(file, 27) - print(functions) assert len(functions) == 1 function = functions.pop() assert isinstance(function, Function) @@ -79,13 +78,13 @@ def _sort_references_lines(refs: list) -> list: return sorted([ref.lines[0] for ref in refs]) -def _test_references_user_defined_aliases(): +def test_references_user_defined_aliases(use_solc_version): """ Tests if references are filled correctly for user defined aliases (declared using "type [...] is [...]" statement). """ - solc_select.switch_global_version("0.8.16", always_install=True) + solc_path = next(use_solc_version("0.8.16")) file = Path(SRC_MAPPING_TEST_ROOT, "ReferencesUserDefinedAliases.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) alias_top_level = slither.compilation_units[0].user_defined_value_types["aliasTopLevel"] assert len(alias_top_level.references) == 2 @@ -102,26 +101,16 @@ def _test_references_user_defined_aliases(): assert lines == [13, 16] -def _test_references_user_defined_types_when_casting(): +def test_references_user_defined_types_when_casting(use_solc_version): """ Tests if references are filled correctly for user defined types in case of casting. """ - solc_select.switch_global_version("0.8.16", always_install=True) + solc_path = next(use_solc_version("0.8.16")) file = Path(SRC_MAPPING_TEST_ROOT, "ReferencesUserDefinedTypesCasting.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) contracts = slither.compilation_units[0].contracts a = contracts[0] if contracts[0].is_interface else contracts[1] assert len(a.references) == 2 lines = _sort_references_lines(a.references) assert lines == [12, 18] - - -def test_references(): - """ - Tests if references list is filled correctly in the following cases: - - user defined aliases (declared using "type [...] is [...]" statement) - - user defined types in case of casting (TypeConversion expressions) - """ - _test_references_user_defined_aliases() - _test_references_user_defined_types_when_casting() diff --git a/tests/unit/core/test_storage_layout.py b/tests/unit/core/test_storage_layout.py index 4cb439d770..fd21ce009c 100644 --- a/tests/unit/core/test_storage_layout.py +++ b/tests/unit/core/test_storage_layout.py @@ -8,14 +8,14 @@ STORAGE_TEST_ROOT = Path(TEST_DATA_DIR, "storage_layout") -def test_storage_layout(): +def test_storage_layout(use_solc_version): # the storage layout has not yet changed between solidity versions so we will test with one version of the compiler - solc_select.switch_global_version("0.8.10", always_install=True) + solc_path = next(use_solc_version("0.8.10")) test_item = Path(STORAGE_TEST_ROOT, "storage_layout-0.8.10.sol").as_posix() - sl = Slither(test_item, solc_force_legacy_json=False, disallow_partial=True) + sl = Slither(test_item, disallow_partial=True, solc=solc_path) - with Popen(["solc", test_item, "--storage-layout"], stdout=PIPE) as process: + with Popen([solc_path, test_item, "--storage-layout"], stdout=PIPE) as process: for line in process.stdout: # parse solc output if '{"storage":[{' in line.decode("utf-8"): # find the storage layout layout = iter(json.loads(line)["storage"]) @@ -34,3 +34,5 @@ def test_storage_layout(): break except KeyError as e: print(f"not found {e} ") + process.communicate() + assert process.returncode == 0 \ No newline at end of file diff --git a/tests/unit/core/test_using_for.py b/tests/unit/core/test_using_for.py index 88a7ea0431..7b0e2d1d66 100644 --- a/tests/unit/core/test_using_for.py +++ b/tests/unit/core/test_using_for.py @@ -12,19 +12,19 @@ USING_FOR_TEST_DATA_DIR = Path(TEST_DATA_DIR, "using_for") -def test_using_for_global_collision() -> None: - solc_select.switch_global_version("0.8.18", always_install=True) +def test_using_for_global_collision(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) standard_json = SolcStandardJson() for source_file in Path(USING_FOR_TEST_DATA_DIR, "using_for_global_collision").rglob("*.sol"): standard_json.add_source_file(Path(source_file).as_posix()) - compilation = CryticCompile(standard_json) + compilation = CryticCompile(standard_json, solc=solc_path) sl = Slither(compilation) _run_all_detectors(sl) -def test_using_for_top_level_same_name() -> None: - solc_select.switch_global_version("0.8.15", always_install=True) - slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-3-0.8.0.sol").as_posix()) +def test_using_for_top_level_same_name(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) + slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-3-0.8.0.sol").as_posix(), solc=solc_path) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint256)") for ir in libCall.all_slithir_operations(): @@ -33,9 +33,9 @@ def test_using_for_top_level_same_name() -> None: assert False -def test_using_for_top_level_implicit_conversion() -> None: - solc_select.switch_global_version("0.8.15", always_install=True) - slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-4-0.8.0.sol").as_posix()) +def test_using_for_top_level_implicit_conversion(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) + slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-4-0.8.0.sol").as_posix(), solc=solc_path) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint16)") for ir in libCall.all_slithir_operations(): @@ -44,10 +44,10 @@ def test_using_for_top_level_implicit_conversion() -> None: assert False -def test_using_for_alias_top_level() -> None: - solc_select.switch_global_version("0.8.15", always_install=True) +def test_using_for_alias_top_level(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) slither = Slither( - Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-top-level-0.8.0.sol").as_posix() + Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-top-level-0.8.0.sol").as_posix(), solc=solc_path ) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint256)") @@ -64,10 +64,10 @@ def test_using_for_alias_top_level() -> None: assert False -def test_using_for_alias_contract() -> None: - solc_select.switch_global_version("0.8.15", always_install=True) +def test_using_for_alias_contract(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) slither = Slither( - Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-contract-0.8.0.sol").as_posix() + Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-contract-0.8.0.sol").as_posix(), solc=solc_path ) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint256)") @@ -84,9 +84,9 @@ def test_using_for_alias_contract() -> None: assert False -def test_using_for_in_library() -> None: - solc_select.switch_global_version("0.8.15", always_install=True) - slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-in-library-0.8.0.sol").as_posix()) +def test_using_for_in_library(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.15")) + slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-in-library-0.8.0.sol").as_posix(), solc=solc_path) contract_c = slither.get_contract_from_name("A")[0] libCall = contract_c.get_function_from_full_name("a(uint256)") for ir in libCall.all_slithir_operations(): diff --git a/tests/unit/slithir/test_operation_reads.py b/tests/unit/slithir/test_operation_reads.py index 3b5565c9f2..10ae474043 100644 --- a/tests/unit/slithir/test_operation_reads.py +++ b/tests/unit/slithir/test_operation_reads.py @@ -28,12 +28,12 @@ def check_num_states_vars_read(function, slithir_op: Operation, num_reads_expect OPERATION_TEST = [OperationTest("NewContract", NewContract)] -def test_operation_reads() -> None: +def test_operation_reads(use_solc_version) -> None: """ Every slithir operation has its own contract and reads all local and state variables in readAllLocalVariables and readAllStateVariables, respectively. """ - solc_select.switch_global_version("0.8.15", always_install=True) - slither = Slither(Path(TEST_DATA_DIR, "operation_reads.sol").as_posix()) + solc_path = next(use_solc_version("0.8.15")) + slither = Slither(Path(TEST_DATA_DIR, "operation_reads.sol").as_posix(), solc=solc_path) for op_test in OPERATION_TEST: print(op_test) diff --git a/tests/unit/slithir/test_ssa_generation.py b/tests/unit/slithir/test_ssa_generation.py index 2146a3126d..34ad5e4fe2 100644 --- a/tests/unit/slithir/test_ssa_generation.py +++ b/tests/unit/slithir/test_ssa_generation.py @@ -1,1090 +1,1071 @@ -# pylint: disable=too-many-lines -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, Dict, Callable - -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.local_variable import LocalVariable -from slither.core.variables.state_variable import StateVariable -from slither.slithir.operations import ( - OperationWithLValue, - Phi, - Assignment, - HighLevelCall, - Return, - Operation, - Binary, - BinaryType, - InternalCall, - Index, - InitArray, -) -from slither.slithir.utils.ssa import is_used_later -from slither.slithir.variables import ( - Constant, - ReferenceVariable, - LocalIRVariable, - StateIRVariable, - TemporaryVariableSSA, -) - -# Directory of currently executing script. Will be used as basis for temporary file names. -SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent # type:ignore - - -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) -> None: - """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, too-many-locals -def ssa_basic_properties(function: Function) -> None: - """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: Dict[str, int] = {} - - for n in function.nodes: - for ir in n.irs: - if isinstance(ir, OperationWithLValue) and ir.lvalue: - name = ir.lvalue.name - if name is None: - continue - 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) - ssa_lvalue = ssa.lvalue - 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: Dict[str, int] = defaultdict(int) - for v in ssa_lvalues: - if v and v.name: - ssa_defs[v.name] += 1 - - for (k, count) in lvalue_assignments.items(): - assert ssa_defs[k] >= count - - # Helper 5/6 - def check_property_5_and_6( - variables: List[LocalVariable], ssavars: List[LocalIRVariable] - ) -> None: - 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) -> None: - """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) -> None: - """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) -> None: - """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: Union[StateIRVariable, LocalIRVariable, TemporaryVariableSSA] - ) -> bool: - """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, - [ - r - for r in ssa.read - if isinstance(r, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA)) - ], - ): - 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): - ssa_lvalue = ssa.lvalue - if isinstance( - ssa_lvalue, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA) - ) and is_used_later(node, ssa_lvalue): - assert have_phi_for_var(df, ssa_lvalue) - - -@contextmanager -def select_solc_version(version: Optional[str]) -> None: - """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)) - solc_select.switch_global_version(version, always_install=True) - yield version - - -@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]) -> None: - """Ensures that basic properties of SSA hold true""" - - def verify_func(func: Function) -> None: - have_ssa_if_ir(func) - phi_values_inserted(func) - ssa_basic_properties(func) - ssa_phi_node_properties(func) - dominance_properties(func) - - def verify(slither: Slither) -> None: - 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: - slither: Slither - with slither_from_source(source_code_or_slither) as slither: - verify(slither) - - -def _dump_function(f: Function) -> None: - """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) -> None: - """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: Callable) -> 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() -> None: - 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() -> None: - 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() -> None: - 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() -> None: - 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.xfail(strict=True, 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 i) { - val = i; - } else { - val = 3; - } - } - return val; - } - } - """ - verify_properties_hold(contract) - - -@pytest.mark.xfail(strict=True, 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 i) { - val = i; - } else { - val = 3; - } - } - return val; - } - - contract Test {} - """ - verify_properties_hold(contract) - - -def test_ssa_inter_transactional() -> None: - 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.xfail(strict=True, 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.xfail(strict=True, 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.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.xfail(strict=True, 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.xfail(strict=True, 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.node.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.xfail(strict=True, 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.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) -def test_memory_array(): - src = """ - contract MemArray { - struct A { - uint val1; - uint val2; - } - - function test_array() internal { - A[] memory a= new A[](4); - // Create REF_0 -> a_1[2] - accept_array_entry(a[2]); - - // Create REF_1 -> a_1[3] - accept_array_entry(a[3]); - - A memory alocal; - accept_array_entry(alocal); - - } - - // val_1 = ϕ(val_0, REF_0, REF_1, alocal_1) - // val_0 is an unknown external value - function accept_array_entry(A memory val) public returns (uint) { - uint zero = 0; - b(zero); - // Create REF_2 -> val_1.val1 - return b(val.val1); - } - - function b(uint arg) public returns (uint){ - // arg_1 = ϕ(arg_0, zero_1, REF_2) - return arg + 1; - } - }""" - with slither_from_source(src) as slither: - c = slither.contracts[0] - - ftest_array, faccept, fb = c.functions - - # Locate REF_0/REF_1/alocal (they are all args to the call) - accept_args = [x.arguments[0] for x in get_ssa_of_type(ftest_array, InternalCall)] - - # Check entrypoint of accept_array_entry, it should contain a phi-node - # of expected rvalues - [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) - for arg in accept_args: - assert arg in phi_entry_accept.rvalues - # NOTE(hbrodin): There should be an additional val_0 in the phi-node. - # That additional val_0 indicates an external caller of this function. - assert len(phi_entry_accept.rvalues) == len(accept_args) + 1 - - # Args used to invoke b - b_args = [x.arguments[0] for x in get_ssa_of_type(faccept, InternalCall)] - - # Check entrypoint of B, it should contain a phi-node of expected - # rvalues - [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) - for arg in b_args: - assert arg in phi_entry_b.rvalues - - # NOTE(hbrodin): There should be an additional arg_0 (see comment about phi_entry_accept). - assert len(phi_entry_b.rvalues) == len(b_args) + 1 - - -@pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) -def test_storage_array(): - src = """ - contract StorageArray { - struct A { - uint val1; - uint val2; - } - - // NOTE(hbrodin): a is never written, should only become a_0. Same for astorage (astorage_0). Phi-nodes at entry - // should only add new versions of a state variable if it is actually written. - A[] a; - A astorage; - - function test_array() internal { - accept_array_entry(a[2]); - accept_array_entry(a[3]); - accept_array_entry(astorage); - } - - function accept_array_entry(A storage val) internal returns (uint) { - // val is either a[2], a[3] or astorage_0. Ideally this could be identified. - uint five = 5; - - // NOTE(hbrodin): If the following line is enabled, there would ideally be a phi-node representing writes - // to either a or astorage. - //val.val2 = 4; - b(five); - return b(val.val1); - } - - function b(uint value) public returns (uint){ - // Expect a phi-node at the entrypoint - // value_1 = ϕ(value_0, five_0, REF_x), where REF_x is the reference to val.val1 in accept_array_entry. - return value + 1; - } - }""" - with slither_from_source(src) as slither: - c = slither.contracts[0] - _dump_functions(c) - ftest, faccept, fb = c.functions - - # None of a/astorage is written so expect that there are no phi-nodes at entrypoint. - assert len(get_ssa_of_type(ftest.entry_point, Phi)) == 0 - - # Expect all references to start from index 0 (no writes) - assert all(x.variable_left.index == 0 for x in get_ssa_of_type(ftest, Index)) - - [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) - assert len(phi_entry_accept.rvalues) == 3 # See comment in b above - - [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) - assert len(phi_entry_b.rvalues) == 3 # See comment in b above - - -@pytest.mark.xfail(strict=True, 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.xfail(strict=True, 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.xfail(strict=True, 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 - - -def test_issue_1748(): - source = """ - contract Contract { - uint[] arr; - function foo(uint i) public { - arr = [1]; - } - } - """ - with slither_from_source(source) as slither: - c = slither.get_contract_from_name("Contract")[0] - f = c.functions[0] - operations = f.slithir_operations - assign_op = operations[0] - assert isinstance(assign_op, InitArray) +# # pylint: disable=too-many-lines +# 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, Dict, Callable + +# 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.local_variable import LocalVariable +# from slither.core.variables.state_variable import StateVariable +# from slither.slithir.operations import ( +# OperationWithLValue, +# Phi, +# Assignment, +# HighLevelCall, +# Return, +# Operation, +# Binary, +# BinaryType, +# InternalCall, +# Index, +# InitArray, +# ) +# from slither.slithir.utils.ssa import is_used_later +# from slither.slithir.variables import ( +# Constant, +# ReferenceVariable, +# LocalIRVariable, +# StateIRVariable, +# TemporaryVariableSSA, +# ) + +# # Directory of currently executing script. Will be used as basis for temporary file names. +# SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent # type:ignore + + +# 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) -> None: +# """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, too-many-locals +# def ssa_basic_properties(function: Function) -> None: +# """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: Dict[str, int] = {} + +# for n in function.nodes: +# for ir in n.irs: +# if isinstance(ir, OperationWithLValue) and ir.lvalue: +# name = ir.lvalue.name +# if name is None: +# continue +# 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) +# ssa_lvalue = ssa.lvalue +# 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: Dict[str, int] = defaultdict(int) +# for v in ssa_lvalues: +# if v and v.name: +# ssa_defs[v.name] += 1 + +# for (k, count) in lvalue_assignments.items(): +# assert ssa_defs[k] >= count + +# # Helper 5/6 +# def check_property_5_and_6( +# variables: List[LocalVariable], ssavars: List[LocalIRVariable] +# ) -> None: +# 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) -> None: +# """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) -> None: +# """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) -> None: +# """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: Union[StateIRVariable, LocalIRVariable, TemporaryVariableSSA] +# ) -> bool: +# """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, +# [ +# r +# for r in ssa.read +# if isinstance(r, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA)) +# ], +# ): +# 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): +# ssa_lvalue = ssa.lvalue +# if isinstance( +# ssa_lvalue, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA) +# ) and is_used_later(node, ssa_lvalue): +# assert have_phi_for_var(df, ssa_lvalue) + + + +# @contextmanager +# def slither_from_source(source_code: str, use_solc_version, solc_version: str = "latest"): +# """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) +# solc_path = use_solc_version(solc_version) +# yield Slither(fname, solc=solc_path) +# finally: +# pathlib.Path(fname).unlink() + + +# def verify_properties_hold(source_code_or_slither: Union[str, Slither]) -> None: +# """Ensures that basic properties of SSA hold true""" + +# def verify_func(func: Function) -> None: +# have_ssa_if_ir(func) +# phi_values_inserted(func) +# ssa_basic_properties(func) +# ssa_phi_node_properties(func) +# dominance_properties(func) + +# def verify(slither: Slither) -> None: +# 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: +# slither: Slither +# with slither_from_source(source_code_or_slither) as slither: +# verify(slither) + + +# def _dump_function(f: Function) -> None: +# """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) -> None: +# """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: Callable) -> 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() -> None: +# 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() -> None: +# 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() -> None: +# 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() -> None: +# 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.xfail(strict=True, 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 i) { +# val = i; +# } else { +# val = 3; +# } +# } +# return val; +# } +# } +# """ +# verify_properties_hold(contract) + + +# @pytest.mark.xfail(strict=True, 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 i) { +# val = i; +# } else { +# val = 3; +# } +# } +# return val; +# } + +# contract Test {} +# """ +# verify_properties_hold(contract) + + +# def test_ssa_inter_transactional() -> None: +# 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.xfail(strict=True, 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.xfail(strict=True, 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.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.xfail(strict=True, 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.xfail(strict=True, 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.node.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.xfail(strict=True, 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.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) +# def test_memory_array(): +# src = """ +# contract MemArray { +# struct A { +# uint val1; +# uint val2; +# } + +# function test_array() internal { +# A[] memory a= new A[](4); +# // Create REF_0 -> a_1[2] +# accept_array_entry(a[2]); + +# // Create REF_1 -> a_1[3] +# accept_array_entry(a[3]); + +# A memory alocal; +# accept_array_entry(alocal); + +# } + +# // val_1 = ϕ(val_0, REF_0, REF_1, alocal_1) +# // val_0 is an unknown external value +# function accept_array_entry(A memory val) public returns (uint) { +# uint zero = 0; +# b(zero); +# // Create REF_2 -> val_1.val1 +# return b(val.val1); +# } + +# function b(uint arg) public returns (uint){ +# // arg_1 = ϕ(arg_0, zero_1, REF_2) +# return arg + 1; +# } +# }""" +# with slither_from_source(src) as slither: +# c = slither.contracts[0] + +# ftest_array, faccept, fb = c.functions + +# # Locate REF_0/REF_1/alocal (they are all args to the call) +# accept_args = [x.arguments[0] for x in get_ssa_of_type(ftest_array, InternalCall)] + +# # Check entrypoint of accept_array_entry, it should contain a phi-node +# # of expected rvalues +# [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) +# for arg in accept_args: +# assert arg in phi_entry_accept.rvalues +# # NOTE(hbrodin): There should be an additional val_0 in the phi-node. +# # That additional val_0 indicates an external caller of this function. +# assert len(phi_entry_accept.rvalues) == len(accept_args) + 1 + +# # Args used to invoke b +# b_args = [x.arguments[0] for x in get_ssa_of_type(faccept, InternalCall)] + +# # Check entrypoint of B, it should contain a phi-node of expected +# # rvalues +# [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) +# for arg in b_args: +# assert arg in phi_entry_b.rvalues + +# # NOTE(hbrodin): There should be an additional arg_0 (see comment about phi_entry_accept). +# assert len(phi_entry_b.rvalues) == len(b_args) + 1 + + +# @pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) +# def test_storage_array(): +# src = """ +# contract StorageArray { +# struct A { +# uint val1; +# uint val2; +# } + +# // NOTE(hbrodin): a is never written, should only become a_0. Same for astorage (astorage_0). Phi-nodes at entry +# // should only add new versions of a state variable if it is actually written. +# A[] a; +# A astorage; + +# function test_array() internal { +# accept_array_entry(a[2]); +# accept_array_entry(a[3]); +# accept_array_entry(astorage); +# } + +# function accept_array_entry(A storage val) internal returns (uint) { +# // val is either a[2], a[3] or astorage_0. Ideally this could be identified. +# uint five = 5; + +# // NOTE(hbrodin): If the following line is enabled, there would ideally be a phi-node representing writes +# // to either a or astorage. +# //val.val2 = 4; +# b(five); +# return b(val.val1); +# } + +# function b(uint value) public returns (uint){ +# // Expect a phi-node at the entrypoint +# // value_1 = ϕ(value_0, five_0, REF_x), where REF_x is the reference to val.val1 in accept_array_entry. +# return value + 1; +# } +# }""" +# with slither_from_source(src) as slither: +# c = slither.contracts[0] +# _dump_functions(c) +# ftest, faccept, fb = c.functions + +# # None of a/astorage is written so expect that there are no phi-nodes at entrypoint. +# assert len(get_ssa_of_type(ftest.entry_point, Phi)) == 0 + +# # Expect all references to start from index 0 (no writes) +# assert all(x.variable_left.index == 0 for x in get_ssa_of_type(ftest, Index)) + +# [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) +# assert len(phi_entry_accept.rvalues) == 3 # See comment in b above + +# [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) +# assert len(phi_entry_b.rvalues) == 3 # See comment in b above + + +# @pytest.mark.xfail(strict=True, 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.xfail(strict=True, 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.xfail(strict=True, 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 + + +# def test_issue_1748(): +# source = """ +# contract Contract { +# uint[] arr; +# function foo(uint i) public { +# arr = [1]; +# } +# } +# """ +# with slither_from_source(source) as slither: +# c = slither.get_contract_from_name("Contract")[0] +# f = c.functions[0] +# operations = f.slithir_operations +# assign_op = operations[0] +# assert isinstance(assign_op, InitArray) diff --git a/tests/unit/slithir/test_ternary_expressions.py b/tests/unit/slithir/test_ternary_expressions.py index 376048e1d5..3fb5159f75 100644 --- a/tests/unit/slithir/test_ternary_expressions.py +++ b/tests/unit/slithir/test_ternary_expressions.py @@ -8,10 +8,10 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" # pylint: disable=too-many-nested-blocks -def test_ternary_conversions() -> None: +def test_ternary_conversions(use_solc_version) -> None: """This tests that true and false sons define the same number of variables that the father node declares""" - solc_select.switch_global_version("0.8.0", always_install=True) - slither = Slither(Path(TEST_DATA_DIR, "ternary_expressions.sol").as_posix()) + solc_path = next(use_solc_version("0.8.0")) + slither = Slither(Path(TEST_DATA_DIR, "ternary_expressions.sol").as_posix(), solc=solc_path) for contract in slither.contracts: for function in contract.functions: vars_declared = 0 diff --git a/tests/unit/utils/test_code_generation.py b/tests/unit/utils/test_code_generation.py index 6794896340..d4c40f42f1 100644 --- a/tests/unit/utils/test_code_generation.py +++ b/tests/unit/utils/test_code_generation.py @@ -9,10 +9,10 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" / "code_generation" -def test_interface_generation() -> None: - solc_select.switch_global_version("0.8.4", always_install=True) +def test_interface_generation(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.4")) - sl = Slither(Path(TEST_DATA_DIR, "CodeGeneration.sol").as_posix()) + sl = Slither(Path(TEST_DATA_DIR, "CodeGeneration.sol").as_posix(), solc=solc_path) actual = generate_interface(sl.get_contract_from_name("TestContract")[0]) expected_path = Path(TEST_DATA_DIR, "TEST_generated_code.sol").as_posix() diff --git a/tests/unit/utils/test_functions_ids.py b/tests/unit/utils/test_functions_ids.py index c944c54731..e6ca9f539b 100644 --- a/tests/unit/utils/test_functions_ids.py +++ b/tests/unit/utils/test_functions_ids.py @@ -41,10 +41,10 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" -def test_functions_ids() -> None: - solc_select.switch_global_version("0.7.0", always_install=True) +def test_functions_ids(use_solc_version) -> None: + solc_path = next(use_solc_version("0.7.0")) file = Path(TEST_DATA_DIR, "functions_ids.sol").as_posix() - sl = Slither(file) + sl = Slither(file, solc=solc_path) contracts_c = sl.get_contract_from_name("C") assert len(contracts_c) == 1 contract_c = contracts_c[0] diff --git a/tests/unit/utils/test_type_helpers.py b/tests/unit/utils/test_type_helpers.py index b6e913d33b..6a71a0a19a 100644 --- a/tests/unit/utils/test_type_helpers.py +++ b/tests/unit/utils/test_type_helpers.py @@ -5,9 +5,9 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" -def test_function_id_rec_structure() -> None: - solc_select.switch_global_version("0.8.0", always_install=True) - slither = Slither(Path(TEST_DATA_DIR, "type_helpers.sol").as_posix()) +def test_function_id_rec_structure(use_solc_version) -> None: + solc_path = next(use_solc_version("0.8.0")) + slither = Slither(Path(TEST_DATA_DIR, "type_helpers.sol").as_posix(), solc=solc_path) for compilation_unit in slither.compilation_units: for function in compilation_unit.functions: assert function.solidity_signature From 07fcb5c1497a985b328beaf2d5a199fe8da92d0a Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 28 Mar 2023 14:18:26 -0500 Subject: [PATCH 02/13] simplify using reviewer suggestions --- Makefile | 9 ++-- tests/conftest.py | 31 +++++-------- tests/e2e/compilation/test_resolution.py | 9 ++-- tests/tools/read-storage/test_read_storage.py | 4 +- tests/unit/core/test_arithmetic.py | 5 +-- tests/unit/core/test_code_comments.py | 18 +++++--- tests/unit/core/test_constant_folding.py | 20 +++++---- tests/unit/core/test_contract_declaration.py | 21 +++++---- tests/unit/core/test_function_declaration.py | 17 ++++---- tests/unit/core/test_source_mapping.py | 12 +++--- tests/unit/core/test_storage_layout.py | 7 ++- tests/unit/core/test_using_for.py | 43 +++++++++++-------- tests/unit/slithir/test_operation_reads.py | 5 +-- tests/unit/slithir/test_ssa_generation.py | 5 +-- .../unit/slithir/test_ternary_expressions.py | 4 +- tests/unit/utils/test_code_generation.py | 5 +-- tests/unit/utils/test_functions_ids.py | 5 +-- tests/unit/utils/test_type_helpers.py | 5 +-- 18 files changed, 112 insertions(+), 113 deletions(-) diff --git a/Makefile b/Makefile index cc102c9586..a94c3eeb87 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ SHELL := /bin/bash PY_MODULE := slither +TEST_MODULE := tests ALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py') \ $(shell find test -name '*.py') @@ -33,7 +34,7 @@ ifneq ($(TESTS),) COV_ARGS := else TEST_ARGS := -n auto - COV_ARGS := --cov-append # --fail-under 100 + COV_ARGS := # --fail-under 100 endif .PHONY: all @@ -56,15 +57,15 @@ $(VENV)/pyvenv.cfg: pyproject.toml .PHONY: lint lint: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ - black --check $(ALL_PY_SRCS) && \ - pylint $(ALL_PY_SRCS) + black --check . && \ + pylint $(PY_MODULE) $(TEST_MODULE) # ruff $(ALL_PY_SRCS) && \ # mypy $(PY_MODULE) && .PHONY: reformat reformat: . $(VENV_BIN)/activate && \ - black $(PY_MODULE) + black . .PHONY: test tests test tests: $(VENV)/pyvenv.cfg diff --git a/tests/conftest.py b/tests/conftest.py index bf15b27d11..63f40b672d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,26 +2,15 @@ from filelock import FileLock from solc_select import solc_select -@pytest.fixture(scope="session") -def solc_versions_installed(): - """List of solc versions available in the test environment.""" - return [] - -@pytest.fixture(scope="session", autouse=True) -def register_solc_versions_installed(solc_versions_installed): - solc_versions_installed.extend(solc_select.installed_versions()) @pytest.fixture(scope="session") -def use_solc_version(request, solc_versions_installed): - def _use_solc_version(version): - print(version) - if version not in solc_versions_installed: - print("Installing solc version", version) - solc_select.install_artifacts([version]) - artifact_path = solc_select.artifact_path(version) - lock = FileLock(artifact_path) - try: - yield artifact_path - finally: - lock.release() - return _use_solc_version +def solc_binary_path(): + def inner(version): + lock = FileLock(f"{version}.lock", timeout=60) + with lock: + if not solc_select.artifact_path(version).exists(): + print("Installing solc version", version) + solc_select.install_artifacts([version]) + return solc_select.artifact_path(version) + + return inner diff --git a/tests/e2e/compilation/test_resolution.py b/tests/e2e/compilation/test_resolution.py index 3444af2e90..71edaa143f 100644 --- a/tests/e2e/compilation/test_resolution.py +++ b/tests/e2e/compilation/test_resolution.py @@ -3,7 +3,6 @@ from crytic_compile import CryticCompile from crytic_compile.platform.solc_standard_json import SolcStandardJson -from solc_select import solc_select from slither import Slither @@ -24,8 +23,8 @@ def test_node_modules() -> None: _run_all_detectors(slither) -def test_contract_name_collision(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.0")) +def test_contract_name_collision(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.0") standard_json = SolcStandardJson() standard_json.add_source_file( Path(TEST_DATA_DIR, "test_contract_name_collisions", "a.sol").as_posix() @@ -40,7 +39,7 @@ def test_contract_name_collision(use_solc_version) -> None: _run_all_detectors(slither) -def test_cycle(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.0")) +def test_cycle(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.0") slither = Slither(Path(TEST_DATA_DIR, "test_cyclic_import", "a.sol").as_posix(), solc=solc_path) _run_all_detectors(slither) diff --git a/tests/tools/read-storage/test_read_storage.py b/tests/tools/read-storage/test_read_storage.py index 3b83df8558..6d2ab007dd 100644 --- a/tests/tools/read-storage/test_read_storage.py +++ b/tests/tools/read-storage/test_read_storage.py @@ -90,8 +90,8 @@ def deploy_contract(w3, ganache, contract_bin, contract_abi) -> Contract: # pylint: disable=too-many-locals @pytest.mark.usefixtures("web3", "ganache") -def test_read_storage(web3, ganache, use_solc_version) -> None: - solc_path = next(use_solc_version(version="0.8.10")) +def test_read_storage(web3, ganache, solc_binary_path) -> None: + solc_path = solc_binary_path(version="0.8.10") assert web3.is_connected() bin_path = Path(TEST_DATA_DIR, "StorageLayout.bin").as_posix() diff --git a/tests/unit/core/test_arithmetic.py b/tests/unit/core/test_arithmetic.py index 6e7843ea0d..6de63d7674 100644 --- a/tests/unit/core/test_arithmetic.py +++ b/tests/unit/core/test_arithmetic.py @@ -1,5 +1,4 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither from slither.utils.arithmetic import unchecked_arithemtic_usage @@ -8,8 +7,8 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" / "arithmetic_usage" -def test_arithmetic_usage(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) +def test_arithmetic_usage(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") slither = Slither(Path(TEST_DATA_DIR, "test.sol").as_posix(), solc=solc_path) assert { diff --git a/tests/unit/core/test_code_comments.py b/tests/unit/core/test_code_comments.py index 4fbbae658b..2dd07caf0c 100644 --- a/tests/unit/core/test_code_comments.py +++ b/tests/unit/core/test_code_comments.py @@ -8,8 +8,8 @@ CUSTOM_COMMENTS_TEST_DATA_DIR = Path(TEST_DATA_DIR, "custom_comments") -def test_upgradeable_comments(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.10")) +def test_upgradeable_comments(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.10") slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "upgrade.sol").as_posix(), solc=solc_path) compilation_unit = slither.compilation_units[0] proxy = compilation_unit.get_contract_from_name("Proxy")[0] @@ -27,11 +27,13 @@ def test_upgradeable_comments(use_solc_version) -> None: assert v1.upgradeable_version == "version_1" -def test_contract_comments(use_solc_version) -> None: +def test_contract_comments(solc_binary_path) -> None: comments = " @title Test Contract\n @dev Test comment" - solc_path = next(use_solc_version("0.8.10")) - slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix(), solc=solc_path) + solc_path = solc_binary_path("0.8.10") + slither = Slither( + Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix(), solc=solc_path + ) compilation_unit = slither.compilation_units[0] contract = compilation_unit.get_contract_from_name("A")[0] @@ -40,8 +42,10 @@ def test_contract_comments(use_solc_version) -> None: # Old solc versions have a different parsing of comments # the initial space (after *) is also not kept on every line comments = "@title Test Contract\n@dev Test comment" - solc_path = next(use_solc_version("0.5.16")) - slither = Slither(Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix(), solc=solc_path) + solc_path = solc_binary_path("0.5.16") + slither = Slither( + Path(CUSTOM_COMMENTS_TEST_DATA_DIR, "contract_comment.sol").as_posix(), solc=solc_path + ) compilation_unit = slither.compilation_units[0] contract = compilation_unit.get_contract_from_name("A")[0] diff --git a/tests/unit/core/test_constant_folding.py b/tests/unit/core/test_constant_folding.py index d01b35dc0e..a572987a4b 100644 --- a/tests/unit/core/test_constant_folding.py +++ b/tests/unit/core/test_constant_folding.py @@ -6,15 +6,17 @@ CONSTANT_FOLDING_TEST_ROOT = Path(TEST_DATA_DIR, "constant_folding") -def test_constant_folding_unary(use_solc_version): - solc_path = next(use_solc_version("0.8.0")) +def test_constant_folding_unary(solc_binary_path): + solc_path = solc_binary_path("0.8.0") file = Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_unary.sol").as_posix() Slither(file, solc=solc_path) -def test_constant_folding_rational(use_solc_version): - solc_path = next(use_solc_version("0.8.0")) - s = Slither(Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_rational.sol").as_posix(), solc=solc_path) +def test_constant_folding_rational(solc_binary_path): + solc_path = solc_binary_path("0.8.0") + s = Slither( + Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_rational.sol").as_posix(), solc=solc_path + ) contract = s.get_contract_from_name("C")[0] variable_a = contract.get_state_variable_from_name("a") @@ -52,9 +54,11 @@ def test_constant_folding_rational(use_solc_version): assert str(ConstantFolding(variable_g.expression, "int64").result()) == "-7" -def test_constant_folding_binary_expressions(use_solc_version): - solc_path = next(use_solc_version("0.8.0")) - sl = Slither(Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_binop.sol").as_posix(), solc=solc_path) +def test_constant_folding_binary_expressions(solc_binary_path): + solc_path = solc_binary_path("0.8.0") + sl = Slither( + Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_binop.sol").as_posix(), solc=solc_path + ) contract = sl.get_contract_from_name("BinOp")[0] variable_a = contract.get_state_variable_from_name("a") diff --git a/tests/unit/core/test_contract_declaration.py b/tests/unit/core/test_contract_declaration.py index 3c1e7175e0..7760829351 100644 --- a/tests/unit/core/test_contract_declaration.py +++ b/tests/unit/core/test_contract_declaration.py @@ -1,6 +1,5 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither from slither.core.variables.state_variable import StateVariable @@ -9,26 +8,30 @@ CONTRACT_DECL_TEST_ROOT = Path(TEST_DATA_DIR, "contract_declaration") -def test_abstract_contract(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.0")) +def test_abstract_contract(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.0") slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "abstract.sol").as_posix(), solc=solc_path) assert not slither.contracts[0].is_fully_implemented - solc_path = next(use_solc_version("0.5.0")) - slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix(), solc=solc_path) + solc_path = solc_binary_path("0.5.0") + slither = Slither( + Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix(), solc=solc_path + ) assert not slither.contracts[0].is_fully_implemented slither = Slither( Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix(), solc_force_legacy_json=True, - solc=solc_path + solc=solc_path, ) assert not slither.contracts[0].is_fully_implemented -def test_private_variable(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) - slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "private_variable.sol").as_posix(), solc=solc_path) +def test_private_variable(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") + slither = Slither( + Path(CONTRACT_DECL_TEST_ROOT, "private_variable.sol").as_posix(), solc=solc_path + ) contract_c = slither.get_contract_from_name("C")[0] f = contract_c.functions[0] var_read = f.variables_read[0] diff --git a/tests/unit/core/test_function_declaration.py b/tests/unit/core/test_function_declaration.py index 4faa9d919c..651f449de5 100644 --- a/tests/unit/core/test_function_declaration.py +++ b/tests/unit/core/test_function_declaration.py @@ -5,7 +5,6 @@ and that these objects behave correctly. """ from pathlib import Path -from solc_select import solc_select from slither import Slither from slither.core.declarations.function import FunctionType @@ -15,9 +14,9 @@ FUNC_DELC_TEST_ROOT = Path(TEST_DATA_DIR, "function_declaration") -def test_functions(use_solc_version): +def test_functions(solc_binary_path): # pylint: disable=too-many-statements - solc_path = next(use_solc_version("0.6.12")) + solc_path = solc_binary_path("0.6.12") file = Path(FUNC_DELC_TEST_ROOT, "test_function.sol").as_posix() slither = Slither(file, solc=solc_path) functions = slither.get_contract_from_name("TestFunction")[0].available_functions_as_dict() @@ -248,8 +247,8 @@ def test_functions(use_solc_version): assert f.return_type[0] == ElementaryType("bool") -def test_function_can_send_eth(use_solc_version): - solc_path = next(use_solc_version("0.6.12")) +def test_function_can_send_eth(solc_binary_path): + solc_path = solc_binary_path("0.6.12") file = Path(FUNC_DELC_TEST_ROOT, "test_function.sol").as_posix() slither = Slither(file, solc=solc_path) compilation_unit = slither.compilation_units[0] @@ -273,8 +272,8 @@ def test_function_can_send_eth(use_solc_version): assert functions["highlevel_call_via_external()"].can_send_eth() is False -def test_reentrant(use_solc_version): - solc_path = next(use_solc_version("0.8.10")) +def test_reentrant(solc_binary_path): + solc_path = solc_binary_path("0.8.10") file = Path(FUNC_DELC_TEST_ROOT, "test_function_reentrant.sol").as_posix() slither = Slither(file, solc=solc_path) compilation_unit = slither.compilation_units[0] @@ -290,8 +289,8 @@ def test_reentrant(use_solc_version): assert functions["internal_and_reentrant()"].is_reentrant -def test_public_variable(use_solc_version) -> None: - solc_path = next(use_solc_version("0.6.12")) +def test_public_variable(solc_binary_path) -> None: + solc_path = solc_binary_path("0.6.12") file = Path(FUNC_DELC_TEST_ROOT, "test_function.sol").as_posix() slither = Slither(file, solc=solc_path) contracts = slither.get_contract_from_name("TestFunction") diff --git a/tests/unit/core/test_source_mapping.py b/tests/unit/core/test_source_mapping.py index 2dd8c24359..1eec9d32a8 100644 --- a/tests/unit/core/test_source_mapping.py +++ b/tests/unit/core/test_source_mapping.py @@ -8,8 +8,8 @@ SRC_MAPPING_TEST_ROOT = Path(TEST_DATA_DIR, "src_mapping") -def test_source_mapping(use_solc_version): - solc_path = next(use_solc_version("0.6.12")) +def test_source_mapping(solc_binary_path): + solc_path = solc_binary_path("0.6.12") file = Path(SRC_MAPPING_TEST_ROOT, "inheritance.sol").as_posix() slither = Slither(file, solc=solc_path) @@ -78,11 +78,11 @@ def _sort_references_lines(refs: list) -> list: return sorted([ref.lines[0] for ref in refs]) -def test_references_user_defined_aliases(use_solc_version): +def test_references_user_defined_aliases(solc_binary_path): """ Tests if references are filled correctly for user defined aliases (declared using "type [...] is [...]" statement). """ - solc_path = next(use_solc_version("0.8.16")) + solc_path = solc_binary_path("0.8.16") file = Path(SRC_MAPPING_TEST_ROOT, "ReferencesUserDefinedAliases.sol").as_posix() slither = Slither(file, solc=solc_path) @@ -101,11 +101,11 @@ def test_references_user_defined_aliases(use_solc_version): assert lines == [13, 16] -def test_references_user_defined_types_when_casting(use_solc_version): +def test_references_user_defined_types_when_casting(solc_binary_path): """ Tests if references are filled correctly for user defined types in case of casting. """ - solc_path = next(use_solc_version("0.8.16")) + solc_path = solc_binary_path("0.8.16") file = Path(SRC_MAPPING_TEST_ROOT, "ReferencesUserDefinedTypesCasting.sol").as_posix() slither = Slither(file, solc=solc_path) diff --git a/tests/unit/core/test_storage_layout.py b/tests/unit/core/test_storage_layout.py index fd21ce009c..3337eb0f74 100644 --- a/tests/unit/core/test_storage_layout.py +++ b/tests/unit/core/test_storage_layout.py @@ -1,16 +1,15 @@ import json from pathlib import Path from subprocess import PIPE, Popen -from solc_select import solc_select from slither import Slither TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" STORAGE_TEST_ROOT = Path(TEST_DATA_DIR, "storage_layout") -def test_storage_layout(use_solc_version): +def test_storage_layout(solc_binary_path): # the storage layout has not yet changed between solidity versions so we will test with one version of the compiler - solc_path = next(use_solc_version("0.8.10")) + solc_path = solc_binary_path("0.8.10") test_item = Path(STORAGE_TEST_ROOT, "storage_layout-0.8.10.sol").as_posix() sl = Slither(test_item, disallow_partial=True, solc=solc_path) @@ -35,4 +34,4 @@ def test_storage_layout(use_solc_version): except KeyError as e: print(f"not found {e} ") process.communicate() - assert process.returncode == 0 \ No newline at end of file + assert process.returncode == 0 diff --git a/tests/unit/core/test_using_for.py b/tests/unit/core/test_using_for.py index 7b0e2d1d66..ebba72eeff 100644 --- a/tests/unit/core/test_using_for.py +++ b/tests/unit/core/test_using_for.py @@ -1,7 +1,6 @@ from pathlib import Path from crytic_compile import CryticCompile from crytic_compile.platform.solc_standard_json import SolcStandardJson -from solc_select import solc_select from slither import Slither from slither.slithir.operations import InternalCall, LibraryCall @@ -12,8 +11,8 @@ USING_FOR_TEST_DATA_DIR = Path(TEST_DATA_DIR, "using_for") -def test_using_for_global_collision(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) +def test_using_for_global_collision(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") standard_json = SolcStandardJson() for source_file in Path(USING_FOR_TEST_DATA_DIR, "using_for_global_collision").rglob("*.sol"): standard_json.add_source_file(Path(source_file).as_posix()) @@ -22,9 +21,11 @@ def test_using_for_global_collision(use_solc_version) -> None: _run_all_detectors(sl) -def test_using_for_top_level_same_name(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) - slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-3-0.8.0.sol").as_posix(), solc=solc_path) +def test_using_for_top_level_same_name(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") + slither = Slither( + Path(USING_FOR_TEST_DATA_DIR, "using-for-3-0.8.0.sol").as_posix(), solc=solc_path + ) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint256)") for ir in libCall.all_slithir_operations(): @@ -33,9 +34,11 @@ def test_using_for_top_level_same_name(use_solc_version) -> None: assert False -def test_using_for_top_level_implicit_conversion(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) - slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-4-0.8.0.sol").as_posix(), solc=solc_path) +def test_using_for_top_level_implicit_conversion(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") + slither = Slither( + Path(USING_FOR_TEST_DATA_DIR, "using-for-4-0.8.0.sol").as_posix(), solc=solc_path + ) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint16)") for ir in libCall.all_slithir_operations(): @@ -44,10 +47,11 @@ def test_using_for_top_level_implicit_conversion(use_solc_version) -> None: assert False -def test_using_for_alias_top_level(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) +def test_using_for_alias_top_level(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") slither = Slither( - Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-top-level-0.8.0.sol").as_posix(), solc=solc_path + Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-top-level-0.8.0.sol").as_posix(), + solc=solc_path, ) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint256)") @@ -64,10 +68,11 @@ def test_using_for_alias_top_level(use_solc_version) -> None: assert False -def test_using_for_alias_contract(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) +def test_using_for_alias_contract(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") slither = Slither( - Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-contract-0.8.0.sol").as_posix(), solc=solc_path + Path(USING_FOR_TEST_DATA_DIR, "using-for-alias-contract-0.8.0.sol").as_posix(), + solc=solc_path, ) contract_c = slither.get_contract_from_name("C")[0] libCall = contract_c.get_function_from_full_name("libCall(uint256)") @@ -84,9 +89,11 @@ def test_using_for_alias_contract(use_solc_version) -> None: assert False -def test_using_for_in_library(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.15")) - slither = Slither(Path(USING_FOR_TEST_DATA_DIR, "using-for-in-library-0.8.0.sol").as_posix(), solc=solc_path) +def test_using_for_in_library(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") + slither = Slither( + Path(USING_FOR_TEST_DATA_DIR, "using-for-in-library-0.8.0.sol").as_posix(), solc=solc_path + ) contract_c = slither.get_contract_from_name("A")[0] libCall = contract_c.get_function_from_full_name("a(uint256)") for ir in libCall.all_slithir_operations(): diff --git a/tests/unit/slithir/test_operation_reads.py b/tests/unit/slithir/test_operation_reads.py index 10ae474043..fc3018cc80 100644 --- a/tests/unit/slithir/test_operation_reads.py +++ b/tests/unit/slithir/test_operation_reads.py @@ -1,6 +1,5 @@ from pathlib import Path from collections import namedtuple -from solc_select import solc_select from slither import Slither from slither.slithir.operations import Operation, NewContract @@ -28,11 +27,11 @@ def check_num_states_vars_read(function, slithir_op: Operation, num_reads_expect OPERATION_TEST = [OperationTest("NewContract", NewContract)] -def test_operation_reads(use_solc_version) -> None: +def test_operation_reads(solc_binary_path) -> None: """ Every slithir operation has its own contract and reads all local and state variables in readAllLocalVariables and readAllStateVariables, respectively. """ - solc_path = next(use_solc_version("0.8.15")) + solc_path = solc_binary_path("0.8.15") slither = Slither(Path(TEST_DATA_DIR, "operation_reads.sol").as_posix(), solc=solc_path) for op_test in OPERATION_TEST: diff --git a/tests/unit/slithir/test_ssa_generation.py b/tests/unit/slithir/test_ssa_generation.py index 34ad5e4fe2..865e3263d0 100644 --- a/tests/unit/slithir/test_ssa_generation.py +++ b/tests/unit/slithir/test_ssa_generation.py @@ -230,9 +230,8 @@ # assert have_phi_for_var(df, ssa_lvalue) - # @contextmanager -# def slither_from_source(source_code: str, use_solc_version, solc_version: str = "latest"): +# def slither_from_source(source_code: str, solc_binary_path, solc_version: str = "latest"): # """Yields a Slither instance using source_code string and solc_version # Creates a temporary file and changes the solc-version temporary to solc_version. @@ -243,7 +242,7 @@ # with NamedTemporaryFile(dir=SCRIPT_DIR, mode="w", suffix=".sol", delete=False) as f: # fname = f.name # f.write(source_code) -# solc_path = use_solc_version(solc_version) +# solc_path = solc_binary_path(solc_version) # yield Slither(fname, solc=solc_path) # finally: # pathlib.Path(fname).unlink() diff --git a/tests/unit/slithir/test_ternary_expressions.py b/tests/unit/slithir/test_ternary_expressions.py index 3fb5159f75..68a6089ccf 100644 --- a/tests/unit/slithir/test_ternary_expressions.py +++ b/tests/unit/slithir/test_ternary_expressions.py @@ -8,9 +8,9 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" # pylint: disable=too-many-nested-blocks -def test_ternary_conversions(use_solc_version) -> None: +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 = next(use_solc_version("0.8.0")) + 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: for function in contract.functions: diff --git a/tests/unit/utils/test_code_generation.py b/tests/unit/utils/test_code_generation.py index d4c40f42f1..35f6cea0ed 100644 --- a/tests/unit/utils/test_code_generation.py +++ b/tests/unit/utils/test_code_generation.py @@ -1,5 +1,4 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither from slither.utils.code_generation import ( @@ -9,8 +8,8 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" / "code_generation" -def test_interface_generation(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.4")) +def test_interface_generation(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.4") sl = Slither(Path(TEST_DATA_DIR, "CodeGeneration.sol").as_posix(), solc=solc_path) diff --git a/tests/unit/utils/test_functions_ids.py b/tests/unit/utils/test_functions_ids.py index e6ca9f539b..23888774b1 100644 --- a/tests/unit/utils/test_functions_ids.py +++ b/tests/unit/utils/test_functions_ids.py @@ -1,5 +1,4 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither # % solc functions_ids.sol --hashes @@ -41,8 +40,8 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" -def test_functions_ids(use_solc_version) -> None: - solc_path = next(use_solc_version("0.7.0")) +def test_functions_ids(solc_binary_path) -> None: + solc_path = solc_binary_path("0.7.0") file = Path(TEST_DATA_DIR, "functions_ids.sol").as_posix() sl = Slither(file, solc=solc_path) contracts_c = sl.get_contract_from_name("C") diff --git a/tests/unit/utils/test_type_helpers.py b/tests/unit/utils/test_type_helpers.py index 6a71a0a19a..420329ab2f 100644 --- a/tests/unit/utils/test_type_helpers.py +++ b/tests/unit/utils/test_type_helpers.py @@ -1,12 +1,11 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" -def test_function_id_rec_structure(use_solc_version) -> None: - solc_path = next(use_solc_version("0.8.0")) +def test_function_id_rec_structure(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.0") slither = Slither(Path(TEST_DATA_DIR, "type_helpers.sol").as_posix(), solc=solc_path) for compilation_unit in slither.compilation_units: for function in compilation_unit.functions: From d8d95b5801da1c9880f688f7e35456eca7a15232 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 29 Mar 2023 10:15:15 -0500 Subject: [PATCH 03/13] add fixture slither_from_source --- tests/conftest.py | 29 +- tests/unit/core/test_code_comments.py | 1 - tests/unit/core/test_constant_folding.py | 4 +- tests/unit/core/test_source_mapping.py | 1 - tests/unit/slithir/test_operation_reads.py | 4 - tests/unit/slithir/test_ssa_generation.py | 2118 ++++++++--------- .../unit/slithir/test_ternary_expressions.py | 5 - tests/unit/utils/test_functions_ids.py | 4 - 8 files changed, 1079 insertions(+), 1087 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 63f40b672d..c6feead550 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,14 @@ +# pylint: disable=redefined-outer-name +from pathlib import Path +from contextlib import contextmanager +from tempfile import NamedTemporaryFile import pytest from filelock import FileLock from solc_select import solc_select +from slither import Slither -@pytest.fixture(scope="session") +@pytest.fixture() def solc_binary_path(): def inner(version): lock = FileLock(f"{version}.lock", timeout=60) @@ -14,3 +19,25 @@ def inner(version): return solc_select.artifact_path(version) return inner + + +@pytest.fixture() +def slither_from_source(solc_binary_path): + @contextmanager + def inner(source_code: str, solc_version: str = "0.8.19"): + """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(mode="w", suffix=".sol", delete=False) as f: + fname = f.name + f.write(source_code) + solc_path = solc_binary_path(solc_version) + yield Slither(fname, solc=solc_path) + finally: + Path(fname).unlink() + + return inner diff --git a/tests/unit/core/test_code_comments.py b/tests/unit/core/test_code_comments.py index 2dd07caf0c..a943591dcc 100644 --- a/tests/unit/core/test_code_comments.py +++ b/tests/unit/core/test_code_comments.py @@ -1,5 +1,4 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither diff --git a/tests/unit/core/test_constant_folding.py b/tests/unit/core/test_constant_folding.py index a572987a4b..489b4e0ec1 100644 --- a/tests/unit/core/test_constant_folding.py +++ b/tests/unit/core/test_constant_folding.py @@ -55,9 +55,9 @@ def test_constant_folding_rational(solc_binary_path): def test_constant_folding_binary_expressions(solc_binary_path): - solc_path = solc_binary_path("0.8.0") sl = Slither( - Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_binop.sol").as_posix(), solc=solc_path + Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_binop.sol").as_posix(), + solc=solc_binary_path("0.8.0"), ) contract = sl.get_contract_from_name("BinOp")[0] diff --git a/tests/unit/core/test_source_mapping.py b/tests/unit/core/test_source_mapping.py index 1eec9d32a8..fe53359777 100644 --- a/tests/unit/core/test_source_mapping.py +++ b/tests/unit/core/test_source_mapping.py @@ -1,5 +1,4 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither from slither.core.declarations import Function diff --git a/tests/unit/slithir/test_operation_reads.py b/tests/unit/slithir/test_operation_reads.py index fc3018cc80..b82ef9d48a 100644 --- a/tests/unit/slithir/test_operation_reads.py +++ b/tests/unit/slithir/test_operation_reads.py @@ -47,7 +47,3 @@ def test_operation_reads(solc_binary_path) -> None: local_function = target.get_function_from_signature("readAllLocalVariables()") num_local_vars = len(local_function.local_variables) check_num_local_vars_read(local_function, op_test.slithir_op, num_local_vars) - - -if __name__ == "__main__": - test_operation_reads() diff --git a/tests/unit/slithir/test_ssa_generation.py b/tests/unit/slithir/test_ssa_generation.py index 865e3263d0..4e26aa54d0 100644 --- a/tests/unit/slithir/test_ssa_generation.py +++ b/tests/unit/slithir/test_ssa_generation.py @@ -1,1070 +1,1050 @@ # # pylint: disable=too-many-lines -# 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, Dict, Callable - -# 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.local_variable import LocalVariable -# from slither.core.variables.state_variable import StateVariable -# from slither.slithir.operations import ( -# OperationWithLValue, -# Phi, -# Assignment, -# HighLevelCall, -# Return, -# Operation, -# Binary, -# BinaryType, -# InternalCall, -# Index, -# InitArray, -# ) -# from slither.slithir.utils.ssa import is_used_later -# from slither.slithir.variables import ( -# Constant, -# ReferenceVariable, -# LocalIRVariable, -# StateIRVariable, -# TemporaryVariableSSA, -# ) - -# # Directory of currently executing script. Will be used as basis for temporary file names. -# SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent # type:ignore - - -# 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) -> None: -# """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, too-many-locals -# def ssa_basic_properties(function: Function) -> None: -# """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: Dict[str, int] = {} - -# for n in function.nodes: -# for ir in n.irs: -# if isinstance(ir, OperationWithLValue) and ir.lvalue: -# name = ir.lvalue.name -# if name is None: -# continue -# 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) -# ssa_lvalue = ssa.lvalue -# 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: Dict[str, int] = defaultdict(int) -# for v in ssa_lvalues: -# if v and v.name: -# ssa_defs[v.name] += 1 - -# for (k, count) in lvalue_assignments.items(): -# assert ssa_defs[k] >= count - -# # Helper 5/6 -# def check_property_5_and_6( -# variables: List[LocalVariable], ssavars: List[LocalIRVariable] -# ) -> None: -# 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) -> None: -# """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) -> None: -# """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) -> None: -# """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: Union[StateIRVariable, LocalIRVariable, TemporaryVariableSSA] -# ) -> bool: -# """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, -# [ -# r -# for r in ssa.read -# if isinstance(r, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA)) -# ], -# ): -# 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): -# ssa_lvalue = ssa.lvalue -# if isinstance( -# ssa_lvalue, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA) -# ) and is_used_later(node, ssa_lvalue): -# assert have_phi_for_var(df, ssa_lvalue) - - -# @contextmanager -# def slither_from_source(source_code: str, solc_binary_path, solc_version: str = "latest"): -# """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) -# solc_path = solc_binary_path(solc_version) -# yield Slither(fname, solc=solc_path) -# finally: -# pathlib.Path(fname).unlink() - - -# def verify_properties_hold(source_code_or_slither: Union[str, Slither]) -> None: -# """Ensures that basic properties of SSA hold true""" - -# def verify_func(func: Function) -> None: -# have_ssa_if_ir(func) -# phi_values_inserted(func) -# ssa_basic_properties(func) -# ssa_phi_node_properties(func) -# dominance_properties(func) - -# def verify(slither: Slither) -> None: -# 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: -# slither: Slither -# with slither_from_source(source_code_or_slither) as slither: -# verify(slither) - - -# def _dump_function(f: Function) -> None: -# """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) -> None: -# """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: Callable) -> 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() -> None: -# 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() -> None: -# 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() -> None: -# 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() -> None: -# 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.xfail(strict=True, 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 i) { -# val = i; -# } else { -# val = 3; -# } -# } -# return val; -# } -# } -# """ -# verify_properties_hold(contract) - - -# @pytest.mark.xfail(strict=True, 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 i) { -# val = i; -# } else { -# val = 3; -# } -# } -# return val; -# } - -# contract Test {} -# """ -# verify_properties_hold(contract) - - -# def test_ssa_inter_transactional() -> None: -# 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.xfail(strict=True, 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.xfail(strict=True, 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.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.xfail(strict=True, 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.xfail(strict=True, 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.node.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.xfail(strict=True, 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.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) -# def test_memory_array(): -# src = """ -# contract MemArray { -# struct A { -# uint val1; -# uint val2; -# } - -# function test_array() internal { -# A[] memory a= new A[](4); -# // Create REF_0 -> a_1[2] -# accept_array_entry(a[2]); - -# // Create REF_1 -> a_1[3] -# accept_array_entry(a[3]); - -# A memory alocal; -# accept_array_entry(alocal); - -# } - -# // val_1 = ϕ(val_0, REF_0, REF_1, alocal_1) -# // val_0 is an unknown external value -# function accept_array_entry(A memory val) public returns (uint) { -# uint zero = 0; -# b(zero); -# // Create REF_2 -> val_1.val1 -# return b(val.val1); -# } - -# function b(uint arg) public returns (uint){ -# // arg_1 = ϕ(arg_0, zero_1, REF_2) -# return arg + 1; -# } -# }""" -# with slither_from_source(src) as slither: -# c = slither.contracts[0] - -# ftest_array, faccept, fb = c.functions - -# # Locate REF_0/REF_1/alocal (they are all args to the call) -# accept_args = [x.arguments[0] for x in get_ssa_of_type(ftest_array, InternalCall)] - -# # Check entrypoint of accept_array_entry, it should contain a phi-node -# # of expected rvalues -# [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) -# for arg in accept_args: -# assert arg in phi_entry_accept.rvalues -# # NOTE(hbrodin): There should be an additional val_0 in the phi-node. -# # That additional val_0 indicates an external caller of this function. -# assert len(phi_entry_accept.rvalues) == len(accept_args) + 1 - -# # Args used to invoke b -# b_args = [x.arguments[0] for x in get_ssa_of_type(faccept, InternalCall)] - -# # Check entrypoint of B, it should contain a phi-node of expected -# # rvalues -# [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) -# for arg in b_args: -# assert arg in phi_entry_b.rvalues - -# # NOTE(hbrodin): There should be an additional arg_0 (see comment about phi_entry_accept). -# assert len(phi_entry_b.rvalues) == len(b_args) + 1 - - -# @pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) -# def test_storage_array(): -# src = """ -# contract StorageArray { -# struct A { -# uint val1; -# uint val2; -# } - -# // NOTE(hbrodin): a is never written, should only become a_0. Same for astorage (astorage_0). Phi-nodes at entry -# // should only add new versions of a state variable if it is actually written. -# A[] a; -# A astorage; - -# function test_array() internal { -# accept_array_entry(a[2]); -# accept_array_entry(a[3]); -# accept_array_entry(astorage); -# } - -# function accept_array_entry(A storage val) internal returns (uint) { -# // val is either a[2], a[3] or astorage_0. Ideally this could be identified. -# uint five = 5; - -# // NOTE(hbrodin): If the following line is enabled, there would ideally be a phi-node representing writes -# // to either a or astorage. -# //val.val2 = 4; -# b(five); -# return b(val.val1); -# } - -# function b(uint value) public returns (uint){ -# // Expect a phi-node at the entrypoint -# // value_1 = ϕ(value_0, five_0, REF_x), where REF_x is the reference to val.val1 in accept_array_entry. -# return value + 1; -# } -# }""" -# with slither_from_source(src) as slither: -# c = slither.contracts[0] -# _dump_functions(c) -# ftest, faccept, fb = c.functions - -# # None of a/astorage is written so expect that there are no phi-nodes at entrypoint. -# assert len(get_ssa_of_type(ftest.entry_point, Phi)) == 0 - -# # Expect all references to start from index 0 (no writes) -# assert all(x.variable_left.index == 0 for x in get_ssa_of_type(ftest, Index)) - -# [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) -# assert len(phi_entry_accept.rvalues) == 3 # See comment in b above - -# [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) -# assert len(phi_entry_b.rvalues) == 3 # See comment in b above - - -# @pytest.mark.xfail(strict=True, 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.xfail(strict=True, 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.xfail(strict=True, 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 - - -# def test_issue_1748(): -# source = """ -# contract Contract { -# uint[] arr; -# function foo(uint i) public { -# arr = [1]; -# } -# } -# """ -# with slither_from_source(source) as slither: -# c = slither.get_contract_from_name("Contract")[0] -# f = c.functions[0] -# operations = f.slithir_operations -# assign_op = operations[0] -# assert isinstance(assign_op, InitArray) +import pathlib +from collections import defaultdict +from argparse import ArgumentTypeError +from inspect import getsourcefile +from typing import Union, List, Dict, Callable + +import pytest +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.local_variable import LocalVariable +from slither.core.variables.state_variable import StateVariable +from slither.slithir.operations import ( + OperationWithLValue, + Phi, + Assignment, + HighLevelCall, + Return, + Operation, + Binary, + BinaryType, + InternalCall, + Index, + InitArray, +) +from slither.slithir.utils.ssa import is_used_later +from slither.slithir.variables import ( + Constant, + ReferenceVariable, + LocalIRVariable, + StateIRVariable, + TemporaryVariableSSA, +) + +# Directory of currently executing script. Will be used as basis for temporary file names. +SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent # type:ignore + + +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) -> None: + """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, too-many-locals +def ssa_basic_properties(function: Function) -> None: + """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: Dict[str, int] = {} + + for n in function.nodes: + for ir in n.irs: + if isinstance(ir, OperationWithLValue) and ir.lvalue: + name = ir.lvalue.name + if name is None: + continue + 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) + ssa_lvalue = ssa.lvalue + 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: Dict[str, int] = defaultdict(int) + for v in ssa_lvalues: + if v and v.name: + ssa_defs[v.name] += 1 + + for (k, count) in lvalue_assignments.items(): + assert ssa_defs[k] >= count + + # Helper 5/6 + def check_property_5_and_6( + variables: List[LocalVariable], ssavars: List[LocalIRVariable] + ) -> None: + 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) -> None: + """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) -> None: + """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) -> None: + """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: Union[StateIRVariable, LocalIRVariable, TemporaryVariableSSA] + ) -> bool: + """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, + [ + r + for r in ssa.read + if isinstance(r, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA)) + ], + ): + 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): + ssa_lvalue = ssa.lvalue + if isinstance( + ssa_lvalue, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA) + ) and is_used_later(node, ssa_lvalue): + assert have_phi_for_var(df, ssa_lvalue) + + +def verify_properties_hold(slither: Slither) -> None: + """Ensures that basic properties of SSA hold true""" + + def verify_func(func: Function) -> None: + have_ssa_if_ir(func) + phi_values_inserted(func) + ssa_basic_properties(func) + ssa_phi_node_properties(func) + dominance_properties(func) + + def verify(slither: Slither) -> None: + 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) + + assert isinstance(slither, Slither) + verify(slither) + + +def _dump_function(f: Function) -> None: + """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) -> None: + """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: Callable) -> 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(slither_from_source) -> None: + source = """ + pragma solidity ^0.8.11; + contract Test { + function multi_write(uint val) external pure returns(uint) { + val = 1; + val = 2; + val = 3; + } + }""" + with slither_from_source(source) as slither: + verify_properties_hold(slither) + + +def test_single_branch_phi(slither_from_source) -> None: + source = """ + pragma solidity ^0.8.11; + contract Test { + function single_branch_phi(uint val) external pure returns(uint) { + if (val == 3) { + val = 9; + } + return val; + } + } + """ + with slither_from_source(source) as slither: + verify_properties_hold(slither) + + +def test_basic_phi(slither_from_source) -> None: + source = """ + 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; + } + } + """ + with slither_from_source(source) as slither: + verify_properties_hold(slither) + + +def test_basic_loop_phi(slither_from_source) -> None: + source = """ + 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; + } + } + """ + with slither_from_source(source) as slither: + verify_properties_hold(slither) + + +@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_phi_propagation_loop(slither_from_source): + source = """ + pragma solidity ^0.8.11; + contract Test { + function looping(uint v) external pure returns(uint) { + uint val = 0; + for (uint i=0;i i) { + val = i; + } else { + val = 3; + } + } + return val; + } + } + """ + with slither_from_source(source) as slither: + verify_properties_hold(slither) + + +@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_free_function_properties(slither_from_source): + source = """ + pragma solidity ^0.8.11; + + function free_looping(uint v) returns(uint) { + uint val = 0; + for (uint i=0;i i) { + val = i; + } else { + val = 3; + } + } + return val; + } + + contract Test {} + """ + with slither_from_source(source) as slither: + verify_properties_hold(slither) + + +def test_ssa_inter_transactional(slither_from_source) -> None: + 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.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_ssa_phi_callbacks(slither_from_source): + 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.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_storage_refers_to(slither_from_source): + """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.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(slither_from_source): + """ + 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.xfail(strict=True, 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(slither_from_source): + """ + 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.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_initial_version_exists_for_state_variables_function_assign(slither_from_source): + """ + 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.node.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(slither_from_source): + 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(slither_from_source): + 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.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_multiple_named_args_returns(slither_from_source): + """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.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) +def test_memory_array(slither_from_source): + src = """ + contract MemArray { + struct A { + uint val1; + uint val2; + } + + function test_array() internal { + A[] memory a= new A[](4); + // Create REF_0 -> a_1[2] + accept_array_entry(a[2]); + + // Create REF_1 -> a_1[3] + accept_array_entry(a[3]); + + A memory alocal; + accept_array_entry(alocal); + + } + + // val_1 = ϕ(val_0, REF_0, REF_1, alocal_1) + // val_0 is an unknown external value + function accept_array_entry(A memory val) public returns (uint) { + uint zero = 0; + b(zero); + // Create REF_2 -> val_1.val1 + return b(val.val1); + } + + function b(uint arg) public returns (uint){ + // arg_1 = ϕ(arg_0, zero_1, REF_2) + return arg + 1; + } + }""" + with slither_from_source(src) as slither: + c = slither.contracts[0] + + ftest_array, faccept, fb = c.functions + + # Locate REF_0/REF_1/alocal (they are all args to the call) + accept_args = [x.arguments[0] for x in get_ssa_of_type(ftest_array, InternalCall)] + + # Check entrypoint of accept_array_entry, it should contain a phi-node + # of expected rvalues + [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) + for arg in accept_args: + assert arg in phi_entry_accept.rvalues + # NOTE(hbrodin): There should be an additional val_0 in the phi-node. + # That additional val_0 indicates an external caller of this function. + assert len(phi_entry_accept.rvalues) == len(accept_args) + 1 + + # Args used to invoke b + b_args = [x.arguments[0] for x in get_ssa_of_type(faccept, InternalCall)] + + # Check entrypoint of B, it should contain a phi-node of expected + # rvalues + [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) + for arg in b_args: + assert arg in phi_entry_b.rvalues + + # NOTE(hbrodin): There should be an additional arg_0 (see comment about phi_entry_accept). + assert len(phi_entry_b.rvalues) == len(b_args) + 1 + + +@pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) +def test_storage_array(slither_from_source): + src = """ + contract StorageArray { + struct A { + uint val1; + uint val2; + } + + // NOTE(hbrodin): a is never written, should only become a_0. Same for astorage (astorage_0). Phi-nodes at entry + // should only add new versions of a state variable if it is actually written. + A[] a; + A astorage; + + function test_array() internal { + accept_array_entry(a[2]); + accept_array_entry(a[3]); + accept_array_entry(astorage); + } + + function accept_array_entry(A storage val) internal returns (uint) { + // val is either a[2], a[3] or astorage_0. Ideally this could be identified. + uint five = 5; + + // NOTE(hbrodin): If the following line is enabled, there would ideally be a phi-node representing writes + // to either a or astorage. + //val.val2 = 4; + b(five); + return b(val.val1); + } + + function b(uint value) public returns (uint){ + // Expect a phi-node at the entrypoint + // value_1 = ϕ(value_0, five_0, REF_x), where REF_x is the reference to val.val1 in accept_array_entry. + return value + 1; + } + }""" + with slither_from_source(src) as slither: + c = slither.contracts[0] + _dump_functions(c) + ftest, faccept, fb = c.functions + + # None of a/astorage is written so expect that there are no phi-nodes at entrypoint. + assert len(get_ssa_of_type(ftest.entry_point, Phi)) == 0 + + # Expect all references to start from index 0 (no writes) + assert all(x.variable_left.index == 0 for x in get_ssa_of_type(ftest, Index)) + + [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) + assert len(phi_entry_accept.rvalues) == 3 # See comment in b above + + [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) + assert len(phi_entry_b.rvalues) == 3 # See comment in b above + + +@pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_issue_468(slither_from_source): + """ + 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.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_issue_434(slither_from_source): + 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.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") +def test_issue_473(slither_from_source): + 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 + + +def test_issue_1748(slither_from_source): + source = """ + contract Contract { + uint[] arr; + function foo(uint i) public { + arr = [1]; + } + } + """ + with slither_from_source(source) as slither: + c = slither.get_contract_from_name("Contract")[0] + f = c.functions[0] + operations = f.slithir_operations + assign_op = operations[0] + assert isinstance(assign_op, InitArray) diff --git a/tests/unit/slithir/test_ternary_expressions.py b/tests/unit/slithir/test_ternary_expressions.py index 68a6089ccf..0acd9345d7 100644 --- a/tests/unit/slithir/test_ternary_expressions.py +++ b/tests/unit/slithir/test_ternary_expressions.py @@ -1,5 +1,4 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither from slither.core.cfg.node import NodeType from slither.slithir.operations import Assignment @@ -37,7 +36,3 @@ def test_ternary_conversions(solc_binary_path) -> None: vars_assigned += 1 assert vars_declared == vars_assigned - - -if __name__ == "__main__": - test_ternary_conversions() diff --git a/tests/unit/utils/test_functions_ids.py b/tests/unit/utils/test_functions_ids.py index 23888774b1..9af42ad856 100644 --- a/tests/unit/utils/test_functions_ids.py +++ b/tests/unit/utils/test_functions_ids.py @@ -57,7 +57,3 @@ def test_functions_ids(solc_binary_path) -> None: assert get_function_id(var.solidity_signature) == int(hashes, 16) else: assert get_function_id(func.solidity_signature) == int(hashes, 16) - - -if __name__ == "__main__": - test_functions_ids() From ca6628ac7f6995e0e50b38dd656448b2e1f9c4a4 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 29 Mar 2023 10:55:00 -0500 Subject: [PATCH 04/13] use crytic-compile with fixed window's relpath --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4ec357ac02..10e0deb1ff 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ "prettytable>=0.7.2", "pycryptodome>=3.4.6", # "crytic-compile>=0.3.0", - "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", + "crytic-compile@git+https://github.com/crytic/crytic-compile.git@windows-rel-path#egg=crytic-compile", "web3>=6.0.0", "solc-select@git+https://github.com/crytic/solc-select.git@query-artifact-path#egg=solc-select", ], From 08f865766cc1faf87baadec5773a72a2219775a3 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 30 Mar 2023 09:06:09 -0500 Subject: [PATCH 05/13] fix versions reqs. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 430cac3435..59fd81ab5b 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "packaging", "prettytable>=0.7.2", "pycryptodome>=3.4.6", - "crytic-compile>=0.3.1,<0.4.0", + # "crytic-compile>=0.3.1,<0.4.0", "crytic-compile@git+https://github.com/crytic/crytic-compile.git@windows-rel-path#egg=crytic-compile", "web3>=6.0.0", "solc-select@git+https://github.com/crytic/solc-select.git@query-artifact-path#egg=solc-select", From 0dd62b76cbd764dcaeae0da43417e4b5a27927a7 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 30 Mar 2023 09:43:43 -0500 Subject: [PATCH 06/13] update contributing --- CONTRIBUTING.md | 71 ++++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8568ef7093..c46bf5658e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ If you're unsure where to start, we recommend our [`good first issue`](https://g Bug reports and feature suggestions can be submitted to our issue tracker. For bug reports, attaching the contract that caused the bug will help us in debugging and resolving the issue quickly. If you find a security vulnerability, do not open an issue; email opensource@trailofbits.com instead. ## Questions -Questions can be submitted to the issue tracker, but you may get a faster response if you ask in our [chat room](https://empireslacking.herokuapp.com/) (in the #ethereum channel). +Questions can be submitted to the "Discussions" page, and you may also join our [chat room](https://empireslacking.herokuapp.com/) (in the #ethereum channel). ## Code Slither uses the pull request contribution model. Please make an account on Github, fork this repo, and submit code contributions via pull request. For more documentation, look [here](https://guides.github.com/activities/forking/). @@ -41,7 +41,7 @@ A code walkthrough is available [here](https://www.youtube.com/watch?v=EUl3UlYSl ## Development Environment Instructions for installing a development version of Slither can be found in our [wiki](https://github.com/crytic/slither/wiki/Developer-installation). -To run the unit tests, you need to clone this repository and run `pip install ".[dev]"`. +To run the unit tests, you need to clone this repository and run `make test`. Run a specific test with `make test TESTS=$test_name`. ### Linters @@ -49,37 +49,54 @@ Several linters and security checkers are run on the PRs. To run them locally in the root dir of the repository: -- `pylint slither tests --rcfile pyproject.toml` -- `black . --config pyproject.toml` +- `make lint` + +> Note, this only validates but does not modify the code. + +To automatically reformat the code: + +- `make reformat` We use pylint `2.13.4`, black `22.3.0`. -### Detectors tests +### Testing + +Slither's test suite is divided into three categories end-to-end (`tests/e2e`), unit (`tests/unit`), and tools (`tests/tools/`). + +How do I know what kind of test(s) to write? + +- End-to-end: functionality that requires invoking `Slither` and inspecting some output such as printers and detectors. +- Unit: additions and modifications to objects should be accompanied by a unit test that defines the expected behavior. Aim to write functions in as pure a way as possible such that they are easier to test. +- Tools: tools built on top of Slither (`slither/tools) but not apart of its core functionality + +#### Adding detector tests For each new detector, at least one regression tests must be present. -- Create a test in `tests` -- Update `ALL_TEST` in `tests/test_detectors.py` -- Run `python ./tests/test_detectors.py --generate`. This will generate the json artifacts in `tests/expected_json`. Add the generated files to git. - - If updating an existing detector, identify the respective json artifacts and then delete them, or run `python ./tests/test_detectors.py --overwrite` instead. -- Run `pytest ./tests/test_detectors.py` and check that everything worked. - -To see the tests coverage, run `pytest tests/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`. -To run tests for a specific detector, run `pytest tests/test_detectors.py -k ReentrancyReadBeforeWritten` (the detector's class name is the argument). -To run tests for a specific version, run `pytest tests/test_detectors.py -k 0.7.6`. -The IDs of tests can be inspected using `pytest tests/test_detectors.py --collect-only`. - -### Parser tests -- Create a test in `tests/ast-parsing` -- Run `python ./tests/test_ast_parsing.py --compile`. This will compile the artifact in `tests/ast-parsing/compile`. Add the compiled artifact to git. -- Run `python ./tests/test_ast_parsing.py --generate`. This will generate the json artifacts in `tests/ast-parsing/expected_json`. Add the generated files to git. -- Run `pytest ./tests/test_ast_parsing.py` and check that everything worked. - -To see the tests coverage, run `pytest tests/test_ast_parsing.py --cov=slither/solc_parsing --cov-branch --cov-report html` -To run tests for a specific test case, run `pytest tests/test_ast_parsing.py -k user_defined_value_type` (the filename is the argument). -To run tests for a specific version, run `pytest tests/test_ast_parsing.py -k 0.8.12`. -To run tests for a specific compiler json format, run `pytest tests/test_ast_parsing.py -k legacy` (can be legacy or compact). -The IDs of tests can be inspected using ``pytest tests/test_ast_parsing.py --collect-only`. +1. Create a test in `tests/e2e/detectors` +2. Update `ALL_TEST` in `tests/e2e/detectors/test_detectors.py` +3. Run `python tests/e2e/detectors/test_detectors.py --generate`. This will generate the json artifacts in `tests/expected_json`. Add the generated files to git. If updating an existing detector, identify the respective json artifacts and then delete them, or run `python ./tests/test_detectors.py --overwrite` instead. +4. Run `pytest tests/e2e/detectors/test_detectors.py` and check that everything worked. + +> ##### Helpful commands +> - To see the tests coverage, run `pytest tests/e2e/detectors/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`. +> - To run tests for a specific detector, run `pytest tests/e2e/detectors/test_detectors.py -k ReentrancyReadBeforeWritten`(the detector's class name is the argument). +> - To run tests for a specific version, run `pytest tests/e2e/detectors/test_detectors.py -k 0.7.6`. +> - The IDs of tests can be inspected using `pytest tests/e2e/detectors/test_detectors.py --collect-only`. + +#### Adding parsing tests + +1. Create a test in `tests/e2e/solc_parsing/` +2. Run `python tests/e2e/solc_parsing/test_ast_parsing.py --compile`. This will compile the artifact in `tests/e2e/solc_parsing/compile`. Add the compiled artifact to git. +3. Run `python tests/e2e/solc_parsing/test_ast_parsing.py --generate`. This will generate the json artifacts in `tests/e2e/solc_parsing/expected_json`. Add the generated files to git. +4. Run `pytest tests/e2e/solc_parsing/test_ast_parsing.py` and check that everything worked. + +> ##### Helpful commands +> - To see the tests coverage, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py --cov=slither/solc_parsing --cov-branch --cov-report html` +> - To run tests for a specific test case, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py -k user_defined_value_type` (the filename is the argument). +> - To run tests for a specific version, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py -k 0.8.12`. +> - To run tests for a specific compiler json format, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py -k legacy` (can be legacy or compact). +> - The IDs of tests can be inspected using ``pytest tests/e2e/solc_parsing/test_ast_parsing.py --collect-only`. ### Synchronization with crytic-compile By default, `slither` follows either the latest version of crytic-compile in pip, or `crytic-compile@master` (look for dependencies in [`setup.py`](./setup.py). If crytic-compile development comes with breaking changes, the process to update `slither` is: From 16547c43ec1e9352661993ef6b8c628083a80d4d Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 30 Mar 2023 10:16:59 -0500 Subject: [PATCH 07/13] markdownlint --- CONTRIBUTING.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c46bf5658e..d045ef4124 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,15 +1,19 @@ # Contributing to Slither + First, thanks for your interest in contributing to Slither! We welcome and appreciate all contributions, including bug reports, feature suggestions, tutorials/blog posts, and code improvements. If you're unsure where to start, we recommend our [`good first issue`](https://github.com/crytic/slither/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and [`help wanted`](https://github.com/crytic/slither/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) issue labels. ## Bug reports and feature suggestions + Bug reports and feature suggestions can be submitted to our issue tracker. For bug reports, attaching the contract that caused the bug will help us in debugging and resolving the issue quickly. If you find a security vulnerability, do not open an issue; email opensource@trailofbits.com instead. ## Questions + Questions can be submitted to the "Discussions" page, and you may also join our [chat room](https://empireslacking.herokuapp.com/) (in the #ethereum channel). ## Code + Slither uses the pull request contribution model. Please make an account on Github, fork this repo, and submit code contributions via pull request. For more documentation, look [here](https://guides.github.com/activities/forking/). Some pull request guidelines: @@ -23,6 +27,7 @@ Some pull request guidelines: ## Directory Structure Below is a rough outline of slither's design: + ```text . ├── analyses # Provides additional info such as data dependency @@ -39,6 +44,7 @@ Below is a rough outline of slither's design: A code walkthrough is available [here](https://www.youtube.com/watch?v=EUl3UlYSluU). ## Development Environment + Instructions for installing a development version of Slither can be found in our [wiki](https://github.com/crytic/slither/wiki/Developer-installation). To run the unit tests, you need to clone this repository and run `make test`. Run a specific test with `make test TESTS=$test_name`. @@ -66,22 +72,23 @@ Slither's test suite is divided into three categories end-to-end (`tests/e2e`), How do I know what kind of test(s) to write? - End-to-end: functionality that requires invoking `Slither` and inspecting some output such as printers and detectors. -- Unit: additions and modifications to objects should be accompanied by a unit test that defines the expected behavior. Aim to write functions in as pure a way as possible such that they are easier to test. -- Tools: tools built on top of Slither (`slither/tools) but not apart of its core functionality +- Unit: additions and modifications to objects should be accompanied by a unit test that defines the expected behavior. Aim to write functions in as pure a way as possible such that they are easier to test. +- Tools: tools built on top of Slither (`slither/tools) but not apart of its core functionality #### Adding detector tests For each new detector, at least one regression tests must be present. 1. Create a test in `tests/e2e/detectors` -2. Update `ALL_TEST` in `tests/e2e/detectors/test_detectors.py` +2. Update `ALL_TEST` in `tests/e2e/detectors/test_detectors.py` 3. Run `python tests/e2e/detectors/test_detectors.py --generate`. This will generate the json artifacts in `tests/expected_json`. Add the generated files to git. If updating an existing detector, identify the respective json artifacts and then delete them, or run `python ./tests/test_detectors.py --overwrite` instead. 4. Run `pytest tests/e2e/detectors/test_detectors.py` and check that everything worked. > ##### Helpful commands -> - To see the tests coverage, run `pytest tests/e2e/detectors/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`. -> - To run tests for a specific detector, run `pytest tests/e2e/detectors/test_detectors.py -k ReentrancyReadBeforeWritten`(the detector's class name is the argument). -> - To run tests for a specific version, run `pytest tests/e2e/detectors/test_detectors.py -k 0.7.6`. +> +> - To see the tests coverage, run `pytest tests/e2e/detectors/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`. +> - To run tests for a specific detector, run `pytest tests/e2e/detectors/test_detectors.py -k ReentrancyReadBeforeWritten`(the detector's class name is the argument). +> - To run tests for a specific version, run `pytest tests/e2e/detectors/test_detectors.py -k 0.7.6`. > - The IDs of tests can be inspected using `pytest tests/e2e/detectors/test_detectors.py --collect-only`. #### Adding parsing tests @@ -92,6 +99,7 @@ For each new detector, at least one regression tests must be present. 4. Run `pytest tests/e2e/solc_parsing/test_ast_parsing.py` and check that everything worked. > ##### Helpful commands +> > - To see the tests coverage, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py --cov=slither/solc_parsing --cov-branch --cov-report html` > - To run tests for a specific test case, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py -k user_defined_value_type` (the filename is the argument). > - To run tests for a specific version, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py -k 0.8.12`. @@ -99,7 +107,9 @@ For each new detector, at least one regression tests must be present. > - The IDs of tests can be inspected using ``pytest tests/e2e/solc_parsing/test_ast_parsing.py --collect-only`. ### Synchronization with crytic-compile + By default, `slither` follows either the latest version of crytic-compile in pip, or `crytic-compile@master` (look for dependencies in [`setup.py`](./setup.py). If crytic-compile development comes with breaking changes, the process to update `slither` is: + - Update `slither/setup.py` to point to the related crytic-compile's branch - Create a PR in `slither` and ensure it passes the CI - Once the development branch is merged in `crytic-compile@master`, ensure `slither` follows the `master` branch From 289337c23618e3cf8f96f19df7fc15328a287ecf Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 30 Mar 2023 10:37:14 -0500 Subject: [PATCH 08/13] remove duplicate header --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d045ef4124..686ded908d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,7 +84,7 @@ For each new detector, at least one regression tests must be present. 3. Run `python tests/e2e/detectors/test_detectors.py --generate`. This will generate the json artifacts in `tests/expected_json`. Add the generated files to git. If updating an existing detector, identify the respective json artifacts and then delete them, or run `python ./tests/test_detectors.py --overwrite` instead. 4. Run `pytest tests/e2e/detectors/test_detectors.py` and check that everything worked. -> ##### Helpful commands +> ##### Helpful commands for detector tests > > - To see the tests coverage, run `pytest tests/e2e/detectors/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`. > - To run tests for a specific detector, run `pytest tests/e2e/detectors/test_detectors.py -k ReentrancyReadBeforeWritten`(the detector's class name is the argument). @@ -98,7 +98,7 @@ For each new detector, at least one regression tests must be present. 3. Run `python tests/e2e/solc_parsing/test_ast_parsing.py --generate`. This will generate the json artifacts in `tests/e2e/solc_parsing/expected_json`. Add the generated files to git. 4. Run `pytest tests/e2e/solc_parsing/test_ast_parsing.py` and check that everything worked. -> ##### Helpful commands +> ##### Helpful commands for parsing tests > > - To see the tests coverage, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py --cov=slither/solc_parsing --cov-branch --cov-report html` > - To run tests for a specific test case, run `pytest tests/e2e/solc_parsing/test_ast_parsing.py -k user_defined_value_type` (the filename is the argument). From 6451285f8f62d7fb24d79d52fa47e7039a2f2b0c Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 3 Apr 2023 16:00:03 -0500 Subject: [PATCH 09/13] create tmpdir in master xdist worker and store lockfiles there --- .github/scripts/unit_test_runner.sh | 4 +-- tests/conftest.py | 54 ++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.github/scripts/unit_test_runner.sh b/.github/scripts/unit_test_runner.sh index ad338f342a..92afd0a4ea 100644 --- a/.github/scripts/unit_test_runner.sh +++ b/.github/scripts/unit_test_runner.sh @@ -2,11 +2,11 @@ # used to pass --cov=$path and --cov-append to pytest if [ "$1" != "" ]; then - pytest "$1" tests/unit/ + pytest "$1" tests/unit/ -n auto status_code=$? python -m coverage report else - pytest tests/unit/ + pytest tests/unit/ -n auto status_code=$? fi diff --git a/tests/conftest.py b/tests/conftest.py index c6feead550..930c921950 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,54 @@ # pylint: disable=redefined-outer-name +import os from pathlib import Path +import tempfile +import shutil from contextlib import contextmanager -from tempfile import NamedTemporaryFile import pytest from filelock import FileLock from solc_select import solc_select from slither import Slither -@pytest.fixture() -def solc_binary_path(): +def pytest_configure(config): + """Create a temporary directory for the tests to use.""" + if is_master(): + config.stash["shared_directory"] = tempfile.mkdtemp() + + +def pytest_unconfigure(config): + """Remove the temporary directory after the tests are done.""" + if is_master(): + shutil.rmtree(config.stash["shared_directory"]) + + +def pytest_configure_node(node): + """Configure each worker node with the shared directory.""" + node.workerinput["shared_directory"] = node.config.stash["shared_directory"] + + +def is_master(): + """Returns True if the current process is the master process (which does not have a worker id).""" + return os.environ.get("PYTEST_XDIST_WORKER") is None + + +@pytest.fixture +def shared_directory(request): + """Returns the shared directory for the current process.""" + if is_master(): + return request.config.stash["shared_directory"] + return request.config.workerinput["shared_directory"] + + +@pytest.fixture +def solc_binary_path(shared_directory): + """ + Returns the path to the solc binary for the given version. + If the binary is not installed, it will be installed. + """ + def inner(version): - lock = FileLock(f"{version}.lock", timeout=60) + lock = FileLock(f"{shared_directory}/{version}.lock", timeout=60) with lock: if not solc_select.artifact_path(version).exists(): print("Installing solc version", version) @@ -21,18 +58,17 @@ def inner(version): return inner -@pytest.fixture() +@pytest.fixture def slither_from_source(solc_binary_path): @contextmanager def inner(source_code: str, solc_version: str = "0.8.19"): - """Yields a Slither instance using source_code string and solc_version - - Creates a temporary file and changes the solc-version temporary to solc_version. + """Yields a Slither instance using source_code string and solc_version. + Creates a temporary file and compiles with solc_version. """ fname = "" try: - with NamedTemporaryFile(mode="w", suffix=".sol", delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", suffix=".sol", delete=False) as f: fname = f.name f.write(source_code) solc_path = solc_binary_path(solc_version) From 90d3b740b3576ba2b1f4e50520d1324b7db3e4a6 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 3 Apr 2023 23:17:22 -0500 Subject: [PATCH 10/13] use as_posix for solc binary path --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 930c921950..5ea228fd34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,7 +53,7 @@ def inner(version): if not solc_select.artifact_path(version).exists(): print("Installing solc version", version) solc_select.install_artifacts([version]) - return solc_select.artifact_path(version) + return solc_select.artifact_path(version).as_posix() return inner From 92d3e4ff210bcee97c96a0f77072270ca6840113 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 3 Apr 2023 23:41:16 -0500 Subject: [PATCH 11/13] replace new uses of switch_global_version with solc_binary_path --- tests/unit/core/test_fallback_receive.py | 6 +++--- tests/unit/utils/test_upgradeability_util.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/unit/core/test_fallback_receive.py b/tests/unit/core/test_fallback_receive.py index 505a9dd6fd..f22e2bcefe 100644 --- a/tests/unit/core/test_fallback_receive.py +++ b/tests/unit/core/test_fallback_receive.py @@ -7,10 +7,10 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" -def test_fallback_receive(): - solc_select.switch_global_version("0.6.12", always_install=True) +def test_fallback_receive(solc_binary_path): + solc_path = solc_binary_path("0.6.12") file = Path(TEST_DATA_DIR, "fallback.sol").as_posix() - slither = Slither(file) + slither = Slither(file, solc=solc_path) fake_fallback = slither.get_contract_from_name("FakeFallback")[0] real_fallback = slither.get_contract_from_name("Fallback")[0] diff --git a/tests/unit/utils/test_upgradeability_util.py b/tests/unit/utils/test_upgradeability_util.py index 7d6fb82dad..87db0a911a 100644 --- a/tests/unit/utils/test_upgradeability_util.py +++ b/tests/unit/utils/test_upgradeability_util.py @@ -16,10 +16,10 @@ # pylint: disable=too-many-locals -def test_upgrades_compare() -> None: - solc_select.switch_global_version("0.8.2", always_install=True) +def test_upgrades_compare(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.2") - sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.8.2.sol")) + sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.8.2.sol"), solc=solc_path) v1 = sl.get_contract_from_name("ContractV1")[0] v2 = sl.get_contract_from_name("ContractV2")[0] missing_vars, new_vars, tainted_vars, new_funcs, modified_funcs, tainted_funcs = compare(v1, v2) @@ -37,9 +37,9 @@ def test_upgrades_compare() -> None: ] -def test_upgrades_implementation_var() -> None: - solc_select.switch_global_version("0.8.2", always_install=True) - sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.8.2.sol")) +def test_upgrades_implementation_var(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.2") + sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.8.2.sol"), solc=solc_path) erc_1967_proxy = sl.get_contract_from_name("ERC1967Proxy")[0] storage_proxy = sl.get_contract_from_name("InheritedStorageProxy")[0] @@ -53,8 +53,8 @@ def test_upgrades_implementation_var() -> None: assert target == storage_proxy.get_state_variable_from_name("implementation") assert slot.slot == 1 - solc_select.switch_global_version("0.5.0", always_install=True) - sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.5.0.sol")) + solc_path = solc_binary_path("0.5.0") + sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.5.0.sol"), solc=solc_path) eip_1822_proxy = sl.get_contract_from_name("EIP1822Proxy")[0] # zos_proxy = sl.get_contract_from_name("ZosProxy")[0] From d1b9e8288900d6b7d75025b3f589adec0b68a045 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 3 Apr 2023 23:45:16 -0500 Subject: [PATCH 12/13] lint: unused imports --- tests/unit/core/test_fallback_receive.py | 1 - tests/unit/utils/test_upgradeability_util.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/unit/core/test_fallback_receive.py b/tests/unit/core/test_fallback_receive.py index f22e2bcefe..9b00c5948b 100644 --- a/tests/unit/core/test_fallback_receive.py +++ b/tests/unit/core/test_fallback_receive.py @@ -1,5 +1,4 @@ from pathlib import Path -from solc_select import solc_select from slither import Slither from slither.core.declarations.function import FunctionType diff --git a/tests/unit/utils/test_upgradeability_util.py b/tests/unit/utils/test_upgradeability_util.py index 87db0a911a..1563d31179 100644 --- a/tests/unit/utils/test_upgradeability_util.py +++ b/tests/unit/utils/test_upgradeability_util.py @@ -1,8 +1,6 @@ import os from pathlib import Path -from solc_select import solc_select - from slither import Slither from slither.core.expressions import Literal from slither.utils.upgradeability import ( From 0a6197560156e6b926eb480bc8e943e56b8da76e Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 10 Apr 2023 08:37:12 -0500 Subject: [PATCH 13/13] address reviewer suggestions --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 686ded908d..6e76425ddf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ A code walkthrough is available [here](https://www.youtube.com/watch?v=EUl3UlYSl Instructions for installing a development version of Slither can be found in our [wiki](https://github.com/crytic/slither/wiki/Developer-installation). -To run the unit tests, you need to clone this repository and run `make test`. Run a specific test with `make test TESTS=$test_name`. +To run the unit tests, you need to clone this repository and run `make test`. Run a specific test with `make test TESTS=$test_name`. The names of tests can be obtained with `pytest tests --collect-only`. ### Linters @@ -73,7 +73,7 @@ How do I know what kind of test(s) to write? - End-to-end: functionality that requires invoking `Slither` and inspecting some output such as printers and detectors. - Unit: additions and modifications to objects should be accompanied by a unit test that defines the expected behavior. Aim to write functions in as pure a way as possible such that they are easier to test. -- Tools: tools built on top of Slither (`slither/tools) but not apart of its core functionality +- Tools: tools built on top of Slither (`slither/tools`) but not apart of its core functionality #### Adding detector tests