Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/src/tools/ReadStorage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
44 changes: 40 additions & 4 deletions slither/tools/read_storage/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,43 @@

import json
import argparse
import os

from crytic_compile import cryticparser

from slither import Slither
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.
Expand Down Expand Up @@ -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)
Expand Down
116 changes: 116 additions & 0 deletions tests/tools/read-storage/test_read_storage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import re
import json
import sys
import subprocess
from pathlib import Path

import pytest
Expand All @@ -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"

Expand Down Expand Up @@ -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