High-reuse reference. These are tool behaviors empirically verified during the MVP + Phase 2A implementation of contract-scanner (2026-05-30) — not theory. Several were disproven assumptions that caused silent false-negatives (the worst failure mode for a vuln scanner: it runs, exits 0, finds nothing, and you never know it analyzed the wrong thing). Read this before touching the Slither / source-fetch / compile path. Each item below is “what we believed → what is actually true → the fix.”
Slither + Etherscan standard-JSON: do NOT use --solc-standard-json
slither --solc-standard-json <file> does NOT consume a JSON file
Disproven during implementation. That flag tells Slither to treat the target as a directory to glob — it does not load a standard-JSON input file. Pointing it at Etherscan’s standard-JSON blob does not do what the name suggests.
Correct approach — compile in-process via crytic-compile’s SolcStandardJson Python API:
Strip Etherscan’s double-brace wrapper. Etherscan returns verified standard-JSON sources wrapped in {{ ... }} (double braces). Strip the outer braces to get valid JSON before parsing.
Inject the file-level AST into outputSelection. Etherscan ships outputSelection = {"*": {"*": ["*"]}}, which selects per-contract outputs but omits the file-level AST Slither needs. Add outputSelection["*"][""] = ["ast"] (empty-string key = file-level). Without this, Slither has no AST to analyze.
Materialize sources to disk. crytic-compile’s SolcStandardJson does a filename existence check, so write each source key to a real file on disk before compiling.
Guard against path traversal. Source keys in the JSON are attacker-controlled (they come from the verified contract submitter). A key like ../../etc/whatever.sol would write outside the work dir. Add a path-traversal guard when materializing — resolve and assert the path stays inside the intended directory.
The directory-target heuristic was tried and silently failed
An earlier approach used a “point Slither at a directory and let it figure out the contract” heuristic. It compiled the wrong subtree, missed the vulnerability, and exited 0 — a silent false-negative. Abandoned in favor of the in-process SolcStandardJson compile above. Do not reintroduce a directory-target heuristic.
solc version selection: per-process env var, never the global solc-select use
solc-select use is global and races in a ThreadPool
solc-select use <ver> writes a global version file. With Stage 4 running under a ThreadPoolExecutor (batch mode), concurrent workers stomp each other’s global selection → wrong compiler for the contract → silent miscompile.
Fix — two parts:
Set SOLC_VERSION per-process. solc-select reads the SOLC_VERSION env var before consulting its global version file. So set SOLC_VERSION in the per-analysis environment (e.g. per subprocess / per call) and never call solc-select use. This makes compiler selection race-free.
Serialize in-process Slither with a module lock. In-process Slither analysis is not thread-safe. Even with per-process solc selection, guard the analysis call with a module-level lock so only one Slither analysis runs at a time. The HTTP fetch / discovery stages still parallelize; only the analyze step serializes.
Etherscan V2 API for BSC
Endpoint + key facts (verified live against BSC)
Endpoint:https://api.etherscan.io/v2/api?chainid=56 (chainid 56 = BSC). One unified V2 key works across all chains — Stage 3 is already multi-chain-ready, just change chainid.
Free tier: 3 req/s, 100k/day.
getsourcecode returns Proxy and Implementation fields (used to follow proxies to their implementation).
result is polymorphic — guard before indexing
On error, the response has status == "0" and result is a STRING (an error message). On success, result is a LIST. Indexing result[0] on an error response throws. Branch on status / type before indexing, never assume a list.
Free-tier entitlement for BSC — confirmed, despite the scare
There was a documented fear (carried in the plan) that Etherscan V2 has no free tier for BSC. This was wrong in practice — the user’s free Etherscan key DID work for BSC; entitlement was confirmed live during validation. Standalone bscscan.com keys are deprecated — use the unified Etherscan V2 key, not a legacy BscScan key.
DeFiLlama: BSC TVL lives under chainTvls["Binance"]
BSC is "Binance", never "BSC" — and chainTvls.Binance ≠ global tvl
A protocol’s BSC TVL is chainTvls["Binance"] (the chain key is "Binance", never "BSC" — same gotcha as the discovery funnel, see funnel-from-raw-defillama-scan). This is distinct from the global tvl field, which sums all chains. Filter and score on chainTvls["Binance"], not global tvl — otherwise a multi-chain protocol with a tiny BSC port gets scored on its huge global TVL and wrongly included (or, if you filter the other way, wrongly excluded).
Slither JSON output shape
No code-snippet field; non-zero exit on BOTH failure and findings
No code snippet in the JSON. Slither’s JSON gives you filename + lines[] for each finding but not the source text. To populate the code_snippet CSV column, read the source lines yourself from the materialized file using filename + lines[].
Exit code is ambiguous. Slither exits non-zero on BOTH (a) compile failure AND (b) success-with-findings. You cannot branch on exit code to tell “broke” from “found bugs.” Parse the JSON and branch on its success flag, not the process exit code.
solc-select downloads x86_64-only solc binaries. On Apple Silicon, an arm64 Docker image cannot exec an x86_64 solc → compile silently fails → 0 findings, exit looks fine. Another silent false-negative.
Fix: pin the image to amd64 so solc runs under Rosetta:
FROM --platform=linux/amd64 python:3.12-slim
pytest + uv flat layout
Package import needs an explicit pythonpath
With the uv flat layout (package at repo root, not under src/), pytest can’t import the package by default. Add to pyproject.toml:
[tool.pytest.ini_options]pythonpath = ["."]
Related
contract-scanner — the design doc (workflow, BSC funnel, targets, caveats)