From 7806399ed239f5bc3e8bb5d6191fc46cac1afd97 Mon Sep 17 00:00:00 2001 From: Aravinds2511 Date: Thu, 6 Aug 2026 11:31:23 +0000 Subject: [PATCH] fix: auto-detect solc Standard JSON input in slither-read-storage --- docs/src/tools/ReadStorage.md | 8 +- slither/tools/read_storage/__main__.py | 44 ++++++- tests/tools/read-storage/test_read_storage.py | 116 ++++++++++++++++++ 3 files changed, 163 insertions(+), 5 deletions(-) diff --git a/docs/src/tools/ReadStorage.md b/docs/src/tools/ReadStorage.md index dd7ef28658..b901ca1429 100644 --- a/docs/src/tools/ReadStorage.md +++ b/docs/src/tools/ReadStorage.md @@ -8,7 +8,7 @@ Slither-read-storage is a tool to retrieve the storage slots and values of entir ```shell positional arguments: - contract_source The deployed contract address if verified on etherscan. Prepend project directory for unverified contracts. + contract_source The deployed contract address if verified on etherscan. Prepend project directory, a Solidity file, or a solc Standard JSON input file for unverified contracts. optional arguments: -h, --help show this help message and exit @@ -41,6 +41,12 @@ Retrieve the storage slots of a local contract: slither-read-storage file.sol 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --json storage_layout.json ``` +Retrieve the storage layout from a solc Standard JSON input file (e.g. produced by `solc --standard-json` or other tooling that emits this format), without needing to reconstruct a source tree: + +```shell +slither-read-storage input.json --contract-name MyContract --json storage_layout.json +``` + Retrieve the storage slots of a contract verified on an Etherscan-like platform: ```shell diff --git a/slither/tools/read_storage/__main__.py b/slither/tools/read_storage/__main__.py index 7f694271ec..b07ebee832 100644 --- a/slither/tools/read_storage/__main__.py +++ b/slither/tools/read_storage/__main__.py @@ -4,6 +4,7 @@ import json import argparse +import os from crytic_compile import cryticparser @@ -11,6 +12,35 @@ from slither.exceptions import SlitherError from slither.tools.read_storage.read_storage import SlitherReadStorage, RpcInfo +# Top-level keys expected in a solc Standard JSON *input* file, see +# https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description +_STANDARD_JSON_REQUIRED_KEYS = ("language", "sources") + + +def _is_solc_standard_json(path: str) -> bool: + """Best-effort check for whether `path` is a solc Standard JSON input file. + + This lets users point slither-read-storage directly at a Standard JSON file + (e.g. produced by `solc --standard-json` tooling, or hand-built) instead of + requiring a .sol file or a full project directory. + + Args: + path (str): path that was passed on the command line as the contract source + + Returns: + bool: True if `path` is a file that looks like a solc Standard JSON input + """ + if not path.endswith(".json") or not os.path.isfile(path): + return False + + try: + with open(path, encoding="utf8") as f: + data = json.load(f) + except (OSError, ValueError): + return False + + return isinstance(data, dict) and all(key in data for key in _STANDARD_JSON_REQUIRED_KEYS) + def parse_args() -> argparse.Namespace: """Parse the underlying arguments for the program. @@ -127,13 +157,19 @@ def main() -> None: args = parse_args() if len(args.contract_source) == 2: - # Source code is file.sol or project directory + # Source code is file.sol, project directory, or a solc Standard JSON file source_code, target = args.contract_source - slither = Slither(source_code, **vars(args)) + kwargs = vars(args) + if not kwargs.get("compile_force_framework") and _is_solc_standard_json(source_code): + kwargs["compile_force_framework"] = "solc-json" + slither = Slither(source_code, **kwargs) else: - # Source code is published and retrieved via etherscan + # Source code is published and retrieved via etherscan, or a solc Standard JSON file target = args.contract_source[0] - slither = Slither(target, **vars(args)) + kwargs = vars(args) + if not kwargs.get("compile_force_framework") and _is_solc_standard_json(target): + kwargs["compile_force_framework"] = "solc-json" + slither = Slither(target, **kwargs) if args.contract_name: contracts = slither.get_contract_from_name(args.contract_name) diff --git a/tests/tools/read-storage/test_read_storage.py b/tests/tools/read-storage/test_read_storage.py index 6940043b4b..d97aa61453 100644 --- a/tests/tools/read-storage/test_read_storage.py +++ b/tests/tools/read-storage/test_read_storage.py @@ -1,5 +1,7 @@ import re import json +import sys +import subprocess from pathlib import Path import pytest @@ -8,6 +10,7 @@ from slither import Slither from slither.tools.read_storage import SlitherReadStorage, RpcInfo +from slither.tools.read_storage.__main__ import _is_solc_standard_json TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" @@ -89,3 +92,116 @@ def test_read_storage(test_contract, storage_file, web3, ganache, solc_binary_pa f.write(str(change.t2)) assert not diff + + +# --- Regression tests for GitHub issue #2777 ------------------------------- +# slither-read-storage should accept a solc Standard JSON *input* file +# (https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description) +# directly as its target, without requiring --compile-force-framework solc-json. + + +def _build_standard_json(sol_path: Path) -> dict: + """Wrap a .sol file's contents in a minimal solc Standard JSON input dict.""" + return { + "language": "Solidity", + "sources": {sol_path.name: {"content": get_source_file(sol_path.as_posix())}}, + "settings": { + "outputSelection": { + "*": { + "*": ["abi", "evm.bytecode", "evm.deployedBytecode", "devdoc", "userdoc"], + "": ["ast"], + } + } + }, + } + + +@pytest.mark.parametrize( + "existing_file, expected", + [ + # Not JSON at all + ("StorageLayout.sol", False), + # A JSON file that isn't a solc Standard JSON input (missing "language"/"sources") + ("not_standard.json", False), + # A well-formed solc Standard JSON input + ("standard_json_input.json", True), + ], +) +def test_is_solc_standard_json(tmp_path, existing_file, expected) -> None: + """`_is_solc_standard_json` should only return True for genuine Standard JSON input files, + and must not raise on non-JSON, malformed JSON, or missing files.""" + + if existing_file == "StorageLayout.sol": + path = Path(TEST_DATA_DIR, existing_file).as_posix() + elif existing_file == "not_standard.json": + path = str(tmp_path / existing_file) + with open(path, "w", encoding="utf8") as f: + json.dump({"foo": "bar"}, f) + else: + path = str(tmp_path / existing_file) + standard_json = _build_standard_json(Path(TEST_DATA_DIR, "StorageLayout.sol")) + with open(path, "w", encoding="utf8") as f: + json.dump(standard_json, f) + + assert _is_solc_standard_json(path) is expected + + # A nonexistent path must never raise, and must return False + assert _is_solc_standard_json(str(tmp_path / "does_not_exist.json")) is False + + # Invalid JSON syntax must never raise, and must return False + broken_path = tmp_path / "broken.json" + broken_path.write_text("{not valid json", encoding="utf8") + assert _is_solc_standard_json(str(broken_path)) is False + + +def test_read_storage_from_standard_json(tmp_path, solc_binary_path) -> None: + """slither-read-storage's storage-layout extraction should work identically whether the + contract is given as a plain .sol file or as a solc Standard JSON input file, with no + extra flags required (i.e. Slither must auto-select the solc-json compilation platform). + + Note: the auto-detection lives in the CLI's main(), not in Slither.__init__ itself, so this + exercises the actual slither-read-storage entrypoint (as issue #2777 did) rather than + calling Slither() directly, which would bypass the fix. + """ + + solc_path = solc_binary_path(version="0.8.10") + sol_path = Path(TEST_DATA_DIR, "StorageLayout.sol") + + # Write out a genuine solc Standard JSON input file, the same shape a user would get from + # `solc --standard-json` tooling. + standard_json_path = tmp_path / "StorageLayout.standard-json.json" + with open(standard_json_path, "w", encoding="utf8") as f: + json.dump(_build_standard_json(sol_path), f) + + def layout_via_cli(target: str, out_name: str) -> dict: + out_path = tmp_path / out_name + subprocess.run( + [ + sys.executable, + "-m", + "slither.tools.read_storage", + target, + "--contract-name", + "StorageLayout", + "--solc", + solc_path, + "--json", + str(out_path), + ], + cwd=TEST_DATA_DIR, + check=True, + capture_output=True, + text=True, + ) + with open(out_path, encoding="utf8") as f: + return json.load(f) + + # Baseline: compiling the plain .sol file directly. + expected_layout = layout_via_cli(sol_path.as_posix(), "expected.json") + + # This is the exact scenario from issue #2777: passing a Standard JSON file as the sole + # target, with no --compile-force-framework flag, must work and produce the same layout. + actual_layout = layout_via_cli(standard_json_path.as_posix(), "actual.json") + + diff = DeepDiff(expected_layout, actual_layout, ignore_order=True) + assert not diff