From e00053fccba147e6d9ebad49e189390548a15f7a Mon Sep 17 00:00:00 2001 From: Yuri T Date: Mon, 7 Sep 2026 17:39:12 +0100 Subject: [PATCH 1/2] docs: refresh verification skills and add allowlist workflow --- .claude/skills/add-explorer/SKILL.md | 162 +++------------- .../references/response-mapping.md | 41 ++++ .claude/skills/allowed-diffs/SKILL.md | 43 ++++ .../references/check-immutable-rule.md | 40 ++++ .claude/skills/debug-diff/SKILL.md | 115 +++-------- .../references/diagnostic-recipes.md | 41 ++++ .claude/skills/new-config/SKILL.md | 183 +++--------------- .claude/skills/validate-config/SKILL.md | 143 +++----------- CLAUDE.md | 17 +- 9 files changed, 288 insertions(+), 497 deletions(-) create mode 100644 .claude/skills/add-explorer/references/response-mapping.md create mode 100644 .claude/skills/allowed-diffs/SKILL.md create mode 100644 .claude/skills/allowed-diffs/references/check-immutable-rule.md create mode 100644 .claude/skills/debug-diff/references/diagnostic-recipes.md diff --git a/.claude/skills/add-explorer/SKILL.md b/.claude/skills/add-explorer/SKILL.md index a0f7065..90b9f47 100644 --- a/.claude/skills/add-explorer/SKILL.md +++ b/.claude/skills/add-explorer/SKILL.md @@ -1,155 +1,47 @@ --- name: add-explorer -description: Add support for a new blockchain explorer API to diffyscan. Use when a new chain or explorer type needs to be supported. +description: Adds or repairs Diffyscan explorer API routing and response adapters for a new host, chain or payload format. Use when explorer support is missing or dispatches incorrectly. If an existing adapter supports the deployment, continue with new-config instead of changing code. +argument-hint: "[explorer-name]" disable-model-invocation: true -argument-hint: [explorer-name] --- -Guide through adding support for a new blockchain explorer to diffyscan. +Find the smallest change that supports the requested explorer. Run commands from the repository root. -## Decision tree: do you need code changes? +## 1. Decide whether code is needed -Most new chains need NO code changes. Work through this list in order: +Read the supported-explorer section of [configuration](../../../docs/configuration.md) and `_get_explorer_fetcher` in `diffyscan/utils/explorer.py`. Check the actual host and current official API documentation or a response fixture; a legacy config does not prove current service availability. -1. **Etherscan v2 API (preferred)** -- If the chain is listed on Etherscan's v2 supported chains, create a config with `"explorer_hostname": "api.etherscan.io"` and `"explorer_chain_id": `. Done. No code changes. This uses the endpoint `https://api.etherscan.io/v2/api?chainid=&module=contract&action=getsourcecode&address=`. +1. Existing Etherscan-compatible API: use its supported endpoint configuration. Etherscan v2 uses `api.etherscan.io` plus `explorer_chain_id`. +2. Existing Blockscout host match: config-only. Unrecognized domains fall through to Etherscan, which may return incomplete sources rather than an immediate error. +3. New Blockscout host: use exact hostname equality for a single requested host. Add a separate exact-match branch if necessary; this does not require refactoring existing routes. Use a domain suffix only when subdomain support is part of the task, with a dot boundary. Test near-misses including `evil-`; copying a permissive existing `endswith` pattern would silently broaden the new route. +4. Different response format: add an adapter and dispatch rule, with token behavior based on the API requirements. -2. **Legacy Etherscan-compatible hostname** -- If the chain has its own Etherscan-style API (e.g. `api.basescan.org`, `api-optimistic.etherscan.io`), create a config with just `"explorer_hostname"` (no `explorer_chain_id`). The default fetcher handles it. No code changes. +For config-only work use [new-config](../new-config/SKILL.md). Avoid adding a fetcher for a chain already served by an existing one. -3. **Existing Blockscout domain** -- If the chain uses a Blockscout instance whose domain already ends with one of the recognized suffixes (see dispatcher below), create a config with that hostname. No code changes. +Read [response mapping](references/response-mapping.md) when implementing a fetcher or diagnosing lost source/settings fields; it covers payload keys, argument shapes and normalization pitfalls. -4. **New Blockscout domain** -- If the chain uses Blockscout but with an unrecognized domain, add the domain suffix to `_get_explorer_fetcher()`. One-line change. +## 2. Implement and test -5. **Entirely new API format** -- Only if the explorer has a non-Etherscan, non-Blockscout API do you need a new fetcher function. +Inspect `get_contract_from_explorer`, the selected fetcher and helpers in `diffyscan/utils/explorer.py`. Dispatch returns `(fetcher, requires_token)`; the caller chooses arguments from that flag. Runtime token loading happens before dispatch, even when an adapter does not send a token. -## Architecture: `diffyscan/utils/explorer.py` +Use `fetch` from `diffyscan/utils/http_client.py` to retain shared User-Agent and error handling. Normalize through existing helpers where applicable: -All explorer logic lives in one file. The call flow is: +- `_build_source_files`: primary and additional sources; +- `_build_solc_input`: sources and compiler settings; +- `_build_contract_payload` and `_attach_contract_metadata`: name, compiler, `solcInput`, constructor arguments, EVM version and linked libraries. -``` -get_contract_from_explorer() # public entry point, handles caching - -> _get_explorer_fetcher() # dispatcher: hostname -> (fetcher_fn, requires_token) - -> fetcher(...) # one of the four fetchers below - -> _validate_contract_name() # verify name matches config -``` - -### The dispatcher: `_get_explorer_fetcher(explorer_hostname)` - -This function maps hostnames to fetcher functions using prefix/suffix matching. It returns a tuple of `(fetcher_function, requires_token: bool)`. - -Current routing rules (checked in order): - -| Condition | Fetcher | Token required? | -|---|---|---| -| `hostname.startswith("zksync")` | `_get_contract_from_zksync` | No | -| `hostname.endswith("mantle.xyz")` | `_get_contract_from_mantle` | No | -| `hostname.endswith("lineascan.build")` | `_get_contract_from_etherscan` (token forced to `None`) | No | -| `hostname.endswith(...)` any of: `mode.network`, `blockscout.com`, `swellnetwork.io`, `lisk.com`, `inkonchain.com`, `routescan.io`, `monadvision.com` | `_get_contract_from_blockscout` | No | -| **Default (everything else)** | `_get_contract_from_etherscan` | **Yes** | - -When `requires_token` is True, the caller passes `(token, hostname, address, chain_id)`. When False, the caller passes `(hostname, address)` only. - -### Fetcher 1: `_get_contract_from_etherscan(token, hostname, address, chain_id=None)` - -Handles both Etherscan v2 and legacy Etherscan APIs. - -- **v2 endpoint** (when `chain_id` is set): `https://{hostname}/v2/api?chainid={chain_id}&module=contract&action=getsourcecode&address={address}&apikey={token}` -- **Legacy endpoint** (when `chain_id` is None): `https://{hostname}/api?module=contract&action=getsourcecode&address={address}&apikey={token}` -- Has built-in rate-limit retry logic (up to 5 retries with linear backoff) -- Response format: `{"message": "OK", "result": [{"ContractName": ..., "SourceCode": ..., "CompilerVersion": ..., ...}]}` -- If `SourceCode` starts with `{{`, it is treated as a JSON solc standard input (stripped of outer braces and parsed) -- Otherwise, `_build_source_files()` + `_build_solc_input()` construct the solc input from flat source -- **Limitation:** Flattened (single-file) contracts and contracts verified without standard JSON input may produce incomplete solc inputs. The flat-source path creates a minimal solc input from the single source file plus any `AdditionalSources`, but without the original compiler settings (e.g. remappings, via-IR). This can cause bytecode mismatches even when source diffs are clean. - -### Fetcher 2: `_get_contract_from_blockscout(hostname, address)` - -- Endpoint: `https://{hostname}/api/v2/smart-contracts/{address}` -- No API token needed -- Response is a flat JSON object with fields: `name`, `file_path`, `source_code`, `additional_sources` (list of `{file_path, source_code}`), `compiler_version`, `compiler_settings`, `optimization_enabled`, `optimization_runs`, `constructor_args`, `evm_version`, `external_libraries` (list of `{name, address, ...}`) -- Note: the response uses both `optimization_runs` and `optimizations_runs` (typo in some Blockscout versions); the code checks both - -### Fetcher 3: `_get_contract_from_zksync(hostname, address)` - -- Endpoint: `https://{hostname}/contract_verification/info/{address}` -- No API token needed -- Response: `{"verifiedAt": ..., "request": {"ContractName": ..., "CompilerVersion": ..., "sourceCode": {"sources": ...}}}` -- Returns a minimal contract dict with `name`, `sources`, `compiler` -- does NOT go through `_build_contract_payload` or `_attach_contract_metadata` -- **Outlier:** Unlike the other fetchers, this does not return `solcInput`. Downstream code (`run_source_diff`, `run_bytecode_diff`) expects `contract["solcInput"]`, so zkSync contracts use a different code path. New fetchers should follow the standard shape returned by `_build_contract_payload` (`name`, `compiler`, `solcInput`, plus optional `constructor_arguments`, `evm_version`, `libraries`). - -### Fetcher 4: `_get_contract_from_mantle(hostname, address)` +Preserve available settings and source paths. Distinguish missing constructor metadata from an empty value; resolve libraries to defining files. The zkSync adapter currently returns `sources` without the `solcInput` required downstream: treat this as an existing compatibility gap, not a template. -- Endpoint: `https://{hostname}/api?module=contract&action=getsourcecode&address={address}` -- No API token needed -- Etherscan-like response but uses `FileName` (not `ContractName`) as primary path, and `AdditionalSources` list uses `Filename`/`SourceCode` keys (note capitalization differences) +Add mocked tests in `tests/test_explorer_utils.py` for dispatch, normalized payload, error/unverified responses and relevant metadata. Isolate cache use so prior responses cannot make the tests pass. A routing-only change still needs a regression test. -## Shared helpers - -- **`_build_source_files(primary_path, primary_source, additional_sources, *, path_key, content_key)`** -- Assembles a `{path: {"content": source}}` dict. The `path_key` and `content_key` parameters handle the field name differences between explorers. -- **`_build_solc_input(source_files, *, optimizer_enabled, optimizer_runs, settings=None)`** -- Wraps source files into a standard solc JSON input with optimizer config and output selection. -- **`_build_contract_payload(name, compiler, solc_input, *, constructor_arguments, evm_version, libraries)`** -- Assembles the final contract dict and calls `_attach_contract_metadata()`. -- **`_attach_contract_metadata(contract, source_files, constructor_arguments, evm_version, libraries)`** -- Normalizes and attaches constructor args (hex string without 0x prefix), EVM version (None if "default"), and libraries (resolved to `{path: {name: address}}` format). Libraries can come from explorer response OR from `solcInput.settings.libraries`; both are merged. - -## Steps: adding a new Etherscan v2 chain (no code changes) - -1. Find the chain ID (e.g. from chainlist.org) -2. Create `configs///.yaml`: - ```yaml - contracts: - "": - explorer_hostname: api.etherscan.io - explorer_token_env_var: ETHERSCAN_EXPLORER_TOKEN - explorer_chain_id: - github_repo: - url: https://github.com// - commit: - relative_root: "" - dependencies: {} - ``` -3. Test: `uv run diffyscan --yes --cache-explorer` - -## Steps: adding a new Blockscout domain - -1. Add the domain suffix to the tuple in `_get_explorer_fetcher()` in `diffyscan/utils/explorer.py` (the `any(explorer_hostname.endswith(domain) for domain in [...])` block) -2. Create a config with `"explorer_hostname": ""` -3. Test: `uv run diffyscan --yes --cache-explorer` - -## Steps: adding a completely new explorer type - -1. **Understand the API**: Document the endpoint URL, response shape, and which fields map to contract name, source code, compiler version, optimizer settings, constructor args, EVM version, and libraries. - -2. **Add hostname detection** in `_get_explorer_fetcher()`: Add a new `elif` branch with `startswith()` or `endswith()` matching. Return `(your_fetcher, False)` -- most non-Etherscan explorers do not use API tokens. - -3. **Implement the fetcher** `_get_contract_from_(hostname, address)`: - - Call the explorer API via `fetch(url).json()` - - Validate response (check for verification status, required fields) - - Use `_build_source_files()` to assemble sources (pass the correct `path_key`/`content_key` for the response format) - - Use `_build_solc_input()` to wrap into solc input - - Return via `_build_contract_payload()` to get normalized metadata - - If the fetcher does not need a token, its signature should be `(hostname, address)` (two args). If it does need a token, use `(token, hostname, address, chain_id=None)` (four args) and set `requires_token=True` in the dispatcher. - -4. **Add tests** in `tests/test_explorer_utils.py` -- follow the existing pattern: monkeypatch `fetch` to return a `DummyResponse`, call `get_contract_from_explorer()`, assert the result. - -5. **Add a config** in `configs///`. - -## Config fields reference - -| Field | Required | Description | -|---|---|---| -| `explorer_hostname` | Yes | API hostname (e.g. `api.etherscan.io`, `explorer.mode.network`) | -| `explorer_chain_id` | No | Chain ID for Etherscan v2 API; omit for legacy or non-Etherscan explorers | -| `explorer_token_env_var` | No | Env var name holding the API key (e.g. `ETHERSCAN_EXPLORER_TOKEN`) | - -## Testing +```sh +uv run pytest -q tests/test_explorer_utils.py tests/test_http_client.py +uv run mypy +uv run black --check diffyscan tests +``` -- Unit tests: `tests/test_explorer_utils.py` -- monkeypatch `diffyscan.utils.explorer.fetch` and `CACHE_DIR` -- Integration: `uv run diffyscan --yes --cache-explorer` -- The `--cache-explorer` flag caches responses to `.diffyscan_cache/` so repeated runs do not hit the API +For broader adapter changes run the full suite. With credentials and a deployment fixture, run `uv run diffyscan path/to/config.yaml --json` and inspect source and bytecode results using [JSON output](../../../docs/json-output.md). Mocked parsing tests do not prove end-to-end chain support. -## Checklist +## 3. Document the supported behavior -- [ ] Determined whether code changes are actually needed (most chains: no) -- [ ] If Etherscan v2: config-only with `explorer_hostname` + `explorer_chain_id` -- [ ] If new Blockscout domain: added suffix to `_get_explorer_fetcher()` tuple -- [ ] If new API type: implemented fetcher, added dispatcher rule, used shared helpers -- [ ] Config created in `configs///` -- [ ] Tests added in `tests/test_explorer_utils.py` -- [ ] `.env.example` updated if a new token env var is needed +Update [configuration](../../../docs/configuration.md) when routing changes. Add a deployment config or `.env.example` entry only if needed for the task. Report behavior, tests and any unverified live integration. diff --git a/.claude/skills/add-explorer/references/response-mapping.md b/.claude/skills/add-explorer/references/response-mapping.md new file mode 100644 index 0000000..3427fdc --- /dev/null +++ b/.claude/skills/add-explorer/references/response-mapping.md @@ -0,0 +1,41 @@ +# Explorer response mapping + +Use this checklist when adapting a response fixture. Confirm behavior in `diffyscan/utils/explorer.py` before editing: these are the current parser assumptions, not a guarantee that an external endpoint remains available. + +## Call and return contract + +`get_contract_from_explorer` handles cache lookup, selects the fetcher, checks the contract name and saves the result. A token-using fetcher receives `(token, hostname, address, chain_id)`; a token-free fetcher receives `(hostname, address)`. + +Return `name`, `compiler` and `solcInput` with `sources` and `settings`. Shared helpers attach `constructor_arguments`, `evm_version` and `libraries` when available. Test through the public entry point as well as any new parser helper, with an isolated cache. + +## Etherscan + +The adapter uses `/api` without chain ID and `/v2/api` with chain ID. It reads the first item in `result`, checks `message: NOTOK` and empty results, and handles rate limits through `_fetch_etherscan_response`. + +| Response field | Use | +| --- | --- | +| `ContractName` | Expected name; also the source key for flattened input | +| `CompilerVersion` | Compiler selection | +| `SourceCode` starting with `{{` | Remove one outer brace pair and parse standard JSON | +| Other string `SourceCode` | Build one source under `ContractName`, with `OptimizationUsed` and `Runs` | +| `ConstructorArguments`, `EVMVersion`, `Library` | Normalize through `_build_contract_payload` | + +The flattened Etherscan path does not retain arbitrary original settings or parse `AdditionalSources`. Do not assume it preserves remappings or via-IR. A source key without a file extension is a clue to inspect the original payload, not proof that bytecode cannot match. + +## Blockscout + +The adapter requests `/api/v2/smart-contracts/{address}`. Map primary `file_path` / `source_code` and each `additional_sources` item using those same keys. `name` and `compiler_version` supply identity and compiler. + +Preserve `compiler_settings`; handle both `optimization_runs` and the fallback spelling `optimizations_runs`. Other inputs are `optimization_enabled`, `constructor_args`, `evm_version` and `external_libraries`. + +Test absent name, absent primary source fields, multiple source files and relevant metadata. `constructor_args` being absent and being empty are different cases. + +## Mantle and zkSync + +Mantle reads Etherscan-style `result[0]`, but the primary source path is `FileName`; `AdditionalSources` entries use `Filename` and `SourceCode`. Preserve this capitalization distinction in fixtures. + +The existing zkSync fetcher returns a different shape without `solcInput` and has inconsistent `contractName` / `ContractName` access. Do not copy these assumptions into a new adapter or claim the common comparison flow supports them without a reproducing test and a fix. + +## Library normalization + +`_attach_contract_metadata` merges libraries from explorer fields and `solcInput.settings.libraries`. Resolve a library to the file containing its definition, not a file importing it. Add fixtures for any new library response format and check that solc link references are satisfied. diff --git a/.claude/skills/allowed-diffs/SKILL.md b/.claude/skills/allowed-diffs/SKILL.md new file mode 100644 index 0000000..794eafb --- /dev/null +++ b/.claude/skills/allowed-diffs/SKILL.md @@ -0,0 +1,43 @@ +--- +name: allowed-diffs +description: Adds, reviews or tightens Diffyscan allowed_diffs rules for explained source or bytecode differences, including replacing any wildcards with granular rules. Use when accepting an expected diff or narrowing an exception; use debug-diff first if its cause is unknown. +--- + +Encode an explained difference without accepting unrelated drift. Run commands from the repository root. + +## 1. Establish evidence + +Read the target config, [bytecode comparison](../../../docs/bytecode-comparison.md) and [JSON output](../../../docs/json-output.md). Confirm chain, contract, pinned source and why the difference is intended. If unknown, use [debug-diff](../debug-diff/SKILL.md) before changing policy. + +Use `suggested_rule` and actual diff evidence as a starting point. Suggestions describe observations; they do not justify accepting them. To replace a wildcard, remove only that rule for a diagnostic run or use a temporary config copy, then inspect uncovered differences. Preserve other rules and contract scope. + +## 2. Choose a rule + +Inspect `evaluate_source_rules`, `evaluate_bytecode_rules` and matchers in `diffyscan/utils/allowed_diffs.py` when coverage is unclear. Rules are alternatives: one rule must cover the differences; separate entries do not accumulate coverage. Combine necessary facets within one rule. + +- Prefer source `line_ranges` for exact hunks. Coordinates are 1-based; `count: 0` represents insertion/deletion. `files` accepts future changes throughout named files. +- Prefer bytecode `immutables` with exact on-chain values at compiler-derived offsets when those values explain the difference. `byte_ranges` constrains offsets and lengths but does not pin values there. +- Add `cbor_metadata: true` for explained metadata differences, combined with other necessary facets in the same rule. +- Use `constructor_args` or `constructor_calldata` for an explained alternate simulation, respecting mutual exclusion. Verify the resulting runtime; an override alone does not prove a match. +- Runtime length and string-literal mismatches currently cannot be accepted by granular byte ranges. Report that limitation without silently widening an exception. + +Every rule needs a concrete `reason` describing deployment evidence and intended scope. `any: true` excludes other facets and accepts future drift. Bytecode `any` can also suppress deployment-simulation errors, but not arbitrary compilation or calldata failures. Difficulty reproducing bytecode alone does not justify it. If a wildcard is justified within the requested policy change, document the limitation and align `KNOWN_WILDCARDS` in `tests/test_no_wildcard_regression.py`. Remove stale registry entries when removing wildcards; do not weaken the guard. + +The exclusivity of `any` applies inside one `allowed_diffs` rule. A constructor override in the separate `bytecode_comparison` section can coexist with an `any` rule; that combination is broad policy, not a schema conflict. + +## 3. Verify the result + +Apply [validate-config](../validate-config/SKILL.md), then: + +```sh +uv run pytest -q tests/test_allowed_diffs.py tests/test_no_wildcard_regression.py tests/test_configs.py +uv run diffyscan path/to/config.yaml --json -E -G +``` + +Confirm the intended contract is `allowed` or `exact`, matched reason/facets fit the change, and requested contracts and comparisons remain covered. Check JSON status and errors even with exit code 0. For a new granular rule, evaluate an unrelated hunk, byte offset or immutable value against the matcher and confirm it stays rejected, scoped to the rule's intended guarantees. + +In the CLI JSON report, use `contracts[].bytecode.facets` and `.reason`; `matched_facets` and `matched_rule` are internal result fields, not report keys. + +For a runnable immutable-rule check, adapt [the matcher example](references/check-immutable-rule.md). `evaluate_bytecode_rules` requires a callable analysis provider; each negative case must supply its changed analysis to that callback as well as to the base argument. + +Report evidence, accepted scope, remaining drift protection and checks actually run. If live verification is unavailable, state that the rule has not been verified on chain. diff --git a/.claude/skills/allowed-diffs/references/check-immutable-rule.md b/.claude/skills/allowed-diffs/references/check-immutable-rule.md new file mode 100644 index 0000000..9af5bf2 --- /dev/null +++ b/.claude/skills/allowed-diffs/references/check-immutable-rule.md @@ -0,0 +1,40 @@ +# Check immutable-rule coverage + +This synthetic check demonstrates one immutable plus metadata in a single rule. Adapt offsets, values and regions to the evidence. It tests rule semantics, not a deployment or the reason for accepting a difference. Run from the repository root: + +```python +from diffyscan.utils.allowed_diffs import evaluate_bytecode_rules + +rule = { + "reason": "Synthetic fixture: one immutable and metadata", + "immutables": [{"offset": 32, "value": "0x" + "11" * 32}], + "cbor_metadata": True, +} +base = { + "exact_match": False, + "runtime_mismatch_ranges": [{"offset": 32, "length": 32, "immutable": True}], + "metadata_mismatch": True, + "string_literal_mismatch": False, + "length_mismatch": False, + "immutable_regions": {32: 32}, + "remote_runtime_bytecode": "0x" + "00" * 32 + "11" * 32 + "00" * 8, +} + +def verdict(**changes): + analysis = {**base, **changes} + return evaluate_bytecode_rules( + analysis, [rule], lambda _rule: analysis + )["status"] + +assert verdict() == "allowed" +assert verdict(remote_runtime_bytecode="0x" + "00" * 32 + "22" * 32 + "00" * 8) == "failed" +assert verdict(runtime_mismatch_ranges=[ + *base["runtime_mismatch_ranges"], + {"offset": 64, "length": 1, "immutable": False}, +]) == "failed" +assert verdict(immutable_regions={32: 31}) == "failed" +assert verdict(length_mismatch=True) == "failed" +assert verdict(string_literal_mismatch=True) == "failed" +``` + +For rules with constructor overrides, the provider needs the corresponding simulated analysis; returning the base analysis would not test that behavior. Use the matching patterns in `tests/test_allowed_diffs.py`. diff --git a/.claude/skills/debug-diff/SKILL.md b/.claude/skills/debug-diff/SKILL.md index 1b9da20..30c23a7 100644 --- a/.claude/skills/debug-diff/SKILL.md +++ b/.claude/skills/debug-diff/SKILL.md @@ -1,106 +1,47 @@ --- name: debug-diff -description: Debug a failed diffyscan verification run. Analyzes diffs, identifies root causes. Use when diffyscan exits with non-zero or shows unexpected diffs. -argument-hint: [config-path-or-contract-address] +description: Diagnoses failed or incomplete Diffyscan runs, unexpected source or bytecode differences, compilation failures and explorer or RPC errors. Use also when JSON reports error despite exit code 0. Explained exception changes belong to allowed-diffs; static config review belongs to validate-config. +argument-hint: "[config-path-or-contract-address]" --- -Help debug a failed diffyscan verification. The user may provide a config path, contract address, or describe the failure. +Find the cause while preserving requested verification scope. Run commands from the repository root. -## Diagnostic steps +## 1. Establish the failing run -### 1. Check the digest output -Each run creates a timestamped directory under `digest/`. The timestamp is `int(time.time())` captured at process start (see `diffyscan/utils/constants.py`): -- `digest/{timestamp}/logs.txt` -- full run log (written by `Logger` in `diffyscan/utils/logger.py`) -- `digest/{timestamp}/diffs/{contract_address}/{filename}.html` -- per-file HTML source diff reports +Use the supplied JSON report and its `log_file`, or locate the digest matching the config, command and contract. Do not assume the newest digest belongs to the task. Read [JSON output](../../../docs/json-output.md) for status and partial-result semantics. -Find the most recent run: -```bash -ls -lt digest/ | grep "^d" | head -5 -``` - -Then inspect its contents: -```bash -# View the log -cat digest//logs.txt +When a rerun is needed: -# List HTML diff reports for a specific contract -ls digest//diffs// +```sh +uv run diffyscan path/to/config.yaml --json -E -G --contract 0xADDRESS ``` -### 2. Identify the type of failure - -**Source code diffs** -- files differ between GitHub and the blockchain explorer: -- Check if the `commit` in `github_repo` matches what was actually deployed -- Check if `relative_root` is correct (sources may live in a subdirectory of the repo) -- Check if entries in `dependencies` have the right `commit` hashes and `relative_root` paths -- Look for import path mismatches (flat vs nested -- may need `--support-brownie` for brownie-verified contracts) -- Open the HTML diff files (`digest/{timestamp}/diffs/{address}/*.html`) to see exactly which lines differ -- The report table in logs shows columns: `#`, `Filename`, `Found`, `Diffs`, `Origin`, `Report` - -**Bytecode diffs** -- compiled bytecode does not match on-chain: -- Missing or wrong constructor arguments -- see `constructor_calldata` or `constructor_args` under the `bytecode_comparison` config key -- Missing libraries -- check if the contract uses external libraries that need addresses in `bytecode_comparison.libraries` -- Wrong EVM version -- the explorer may report a different version than expected -- Immutable variables -- `deep_match_bytecode()` in `diffyscan/utils/binary_verifier.py` compares instruction-by-instruction and tolerates differences that fall within known immutable reference regions. If all diffs are in immutable positions it logs a warning and reports a non-match. To accept it, add a granular `allowed_diffs.bytecode` rule with the exact on-chain `immutables` values (diffyscan prints a ready-to-paste suggestion in the final summary) rather than a blanket wildcard. Differences outside immutable regions raise `BinVerifierError`, which `process_config` catches and records as a non-match (`match=False`) — it does not abort the run -- Optimizer settings mismatch -- the solcInput from the explorer includes optimizer settings; the GitHub recompilation must match -- Flat-source contracts -- see **Known limitations** section below - -**Compilation errors** (raised as `CompileError`): -- Missing GitHub sources -- a dependency is not configured or has a wrong `relative_root`. The error message is: `"missing GitHub sources for bytecode compilation; count=N; first=path1, path2..."` (from `run_bytecode_diff()` in `diffyscan/diffyscan.py`) -- Solc version mismatch -- check that the compiler version exists for the platform (solc binaries are cached in `~/.cache/diffyscan/solc/` or equivalent via `XDG_CACHE_HOME`) - -**Constructor calldata errors** (raised as `CalldataError` from `diffyscan/utils/calldata.py`): -- `"No constructor calldata found for 0x... (not in config and not in explorer metadata)"` -- need to add `constructor_calldata` or `constructor_args` to the config -- `"Contract 0x... found in both 'constructor_args' and 'constructor_calldata'"` -- only one should be specified per contract - -**Network/API errors**: -- Explorer API rate limiting -- try `--cache-explorer` to reuse cached responses (cached in `.diffyscan_cache/`) -- RPC errors -- check that `REMOTE_RPC_URL` env var is valid and the node supports `eth_call` -- Explorer token missing -- the config key `explorer_token_env_var` names the env var holding the API token; if absent, falls back to `ETHERSCAN_EXPLORER_TOKEN` - -### 3. Common fixes - -| Symptom | Likely fix | -|---------|-----------| -| `"missing GitHub sources for bytecode compilation"` | Add the missing dependency to `dependencies` in the config with correct `url`, `commit`, and `relative_root` | -| Source diffs in OpenZeppelin or other dependency imports | Check the dependency `commit` hash matches what was used at deploy time | -| Bytecode mismatch after constructor | Add `constructor_calldata` (raw hex) or `constructor_args` (ABI-typed values) for the contract under `bytecode_comparison` | -| `"Failed to infer source path for library '...' from explorer metadata"` | Add library addresses to `bytecode_comparison.libraries` keyed by `"path/to/File.sol": {"LibName": "0xAddr"}` | -| All files show diffs | Wrong `commit` or `relative_root` in `github_repo` | -| Single contract fails | May need a per-contract `constructor_calldata` or `constructor_args` entry | -| `"Failed in binary comparison: Bytecodes have differences not on the immutable reference position"` | Real bytecode mismatch -- check compiler version, optimizer settings, EVM version, and library addresses | -| `"Exiting with non-zero code due to unallowed diffs"` | Either fix the diffs or add a granular `allowed_diffs` rule to the config for known-acceptable diffs (copy the suggestion diffyscan prints in the final summary; prefer `immutables`/`byte_ranges`/`line_ranges`/`files`/`cbor_metadata` over `any: true`) | -| `"Contract name in config does not match with blockchain explorer ... !="` | Contract not verified on explorer — comment it out or verify it on the explorer first | -| `"Failed to get calldata: Explorer metadata has empty constructor calldata for 0x..."` | Factory-created contract — Etherscan has no constructor args. Add `constructor_calldata` manually (extract via `getsourcecode` API `ConstructorArguments`, `debug_traceTransaction` trace, or cross-chain reuse) | -| `"HTTP error: 404 ... contents/.sol"` | Missing or wrong `dependencies` entry — the explorer source uses a path prefix not covered by config dependencies. Add a mapping for that prefix | -| `"err: intrinsic gas too low"` | Chain gas model needs a higher deployment gas cap (known on Mantle) — set `deployment_gas_limit` in the config (e.g. `30000000000`) | -| All proxy bytecode diffs say "immutable reference position" | Normal for TransparentUpgradeableProxy — ProxyAdmin address baked in as immutable. Add an `allowed_diffs.bytecode` rule with the exact `immutables` values (from the printed suggestion) to accept it | - -### 4. Suggest a re-run command -After fixing, suggest: -```bash -uv run diffyscan --yes --cache-explorer --cache-github -``` +Omit the filter for the full config. `--json` implies `--yes`; stdout is the report and tracebacks go to stderr. Read `status`, top-level and contract errors, comparison results and coverage. Exit code 0 alone is insufficient. Missing entries after an abort are not verified contracts. Distinguish an unmatched filter, an empty config and both comparisons disabled from success. + +Caches accelerate diagnosis. Omit `-E` if explorer metadata may be stale; do not delete shared caches indiscriminately. Keep tokens and credential-bearing URLs out of shared output. -- `--yes` (`-Y`) skips the interactive prompt before each contract -- `--cache-explorer` (`-E`) reuses cached explorer responses from `.diffyscan_cache/` -- `--cache-github` (`-G`) reuses cached GitHub file fetches -- `--support-brownie` enables recursive retrieval for brownie-verified contracts with flattened import paths -- `--json` (`-J`) prints one JSON report to stdout instead of logs: per-contract `source`/`bytecode` status, diff hunks, `uncovered` bytecode ranges, error text, and ready `suggested_rule` entries for `allowed_diffs`. Prefer it when re-running to inspect results; the format is specified in `docs/json-output.md`. Check `status`, not only the exit code (a skipped contract makes it `error` even with exit code 0) +Use [diagnostic recipes](references/diagnostic-recipes.md) for locating digest evidence, recovering constructor calldata or following an error-specific check. -To accept known diffs, use config `allowed_diffs` rules. (The former `--allow-source-diff` / `--allow-bytecode-diff` CLI flags have been removed; they were blanket `any: true` shorthands.) When a diff is uncovered, diffyscan prints a ready-to-paste `allowed_diffs` snippet in the final summary — paste it into the config and replace the placeholder `reason`, tightening `any: true` to a granular facet (`immutables`, `byte_ranges`, `cbor_metadata`, `line_ranges`, `files`) wherever possible. See the "Granular allowlists" section of the README. +## 2. Trace the cause -## Known limitations +Read [configuration](../../../docs/configuration.md) for prerequisites and [bytecode comparison](../../../docs/bytecode-comparison.md) for the trust model and overrides. Consult the relevant implementation: -If any of these apply, **explicitly tell the user** — these are inherent constraints of the tool, not bugs to debug. +| Evidence | Check next | +| --- | --- | +| Missing source or GitHub 404 | Commit, `relative_root` and import-prefix resolution in `diffyscan/utils/github.py`; distinguish a wrong path from a missing dependency. | +| Source hunks | Inspect report HTML and actual changes. Confirm deployment provenance before changing the pinned commit. Use `--support-brownie` only for flattened import-path resolution. | +| Compilation error | `run_bytecode_diff` in `diffyscan/diffyscan.py`, compiler/settings, dependencies, `extra_sources` and library definition paths. | +| Calldata or simulation error | `diffyscan/utils/calldata.py`, explorer metadata, constructor ABI, `deployment_from`, RPC state and `deployment_gas_limit`. Recover calldata from exact creation input and ABI, not an address substring or cross-chain address match. | +| Bytecode differences | `analyze_bytecode_diff` in `diffyscan/utils/binary_verifier.py` and evaluation in `diffyscan/utils/allowed_diffs.py`; inspect uncovered ranges, metadata, runtime length and immutable values. | +| Explorer error | Dispatch in `diffyscan/utils/explorer.py`, token lookup, response shape and verification status. A name mismatch may mean the wrong name, address or chain; it does not alone prove the contract is unverified. | +| HTTP challenge or RPC error | `diffyscan/utils/http_client.py`, `DIFFYSCAN_USER_AGENT`, endpoint availability, chain and supported RPC methods. | -### Flat-source (non-standard-JSON) contracts +Flattened submissions may lack settings needed to reproduce bytecode. Inspect the payload and compilation before attributing a mismatch to that limitation. Proxy immutable differences need an explanation for the observed values. Neither case alone justifies a wildcard or dropping bytecode verification. -When a contract was verified on the explorer as a single flattened file (the explorer's `SourceCode` is a plain string, not JSON wrapped in `{{}}`), diffyscan **cannot reliably reproduce the bytecode**. Two problems arise: +Draft exception examples only for observed and explained differences. In particular, flattened source does not by itself establish a metadata mismatch: omit `cbor_metadata` unless that difference is evidenced. Leave unknown causes and values unresolved rather than filling them into a plausible `reason`. -1. **Lost compiler settings** — diffyscan reconstructs a minimal solc input with only basic optimizer settings. Remappings, via-IR, and other original compiler settings are not available from the explorer for flat-verified contracts. Bytecode mismatches are **expected** even when source diffs are clean. -2. **Contract name used as source key** — the explorer's `ContractName` field is used as the solc source file key (e.g. `{"sources": {"MyContract": {...}}}`). If the explorer does not return `ContractName`, or the name does not match the actual `contract` declaration in the Solidity source, the compiled output cannot be remapped and `get_target_compiled_contract()` will fail. +## 3. Fix and verify -**How to detect**: check the explorer response or logs — if the solc input has a source key that looks like a contract name (no `.sol` extension, no path separators) rather than a file path, the contract was flat-verified. +Make the smallest correction supported by evidence. For diagnosis-only requests, report it without editing. For intended differences, use [allowed-diffs](../allowed-diffs/SKILL.md). Keep unresolved contracts in scope instead of commenting them out or disabling comparisons to pass. -**What to tell the user**: explain that bytecode comparison is unreliable for this contract due to the flat-source limitation. Recommend accepting the mismatch with an `allowed_diffs.bytecode` rule (use `any: true` here only because the bytecode genuinely cannot be reproduced — and say so in the `reason`), and note that source comparison is still valid. +Rerun the affected contract after a fix, then the requested scope when available. Report root cause, evidence paths, changed fields, final statuses and unknowns. State any missing credentials or external dependency that prevented live confirmation. diff --git a/.claude/skills/debug-diff/references/diagnostic-recipes.md b/.claude/skills/debug-diff/references/diagnostic-recipes.md new file mode 100644 index 0000000..22945ea --- /dev/null +++ b/.claude/skills/debug-diff/references/diagnostic-recipes.md @@ -0,0 +1,41 @@ +# Diagnostic recipes + +Use the relevant section for an observed failure. Commands run from the repository root; replace placeholders with the target run's values. + +## Locate the evidence + +Each run writes `digest//logs.txt` and source HTML under `digest//diffs/
/`. Prefer the JSON report's `log_file` and source `diffs[].report` fields. If only a config path is known: + +```sh +rg -l --fixed-strings 'Loading config path/to/config.yaml' digest -g logs.txt +rg --files digest//diffs/
+``` + +Compare candidate logs with the requested command, config and contract. A newer unrelated run does not replace the failing run's evidence. + +## Missing or empty constructor calldata + +1. Inspect the constructor ABI and saved explorer metadata. `get_calldata` skips argument handling when the ABI has no constructor inputs. With inputs, an absent metadata value and an empty hex value lead to different diagnostics. +2. Check manual `constructor_args` and `constructor_calldata` for the exact address spelling used in `contracts`. They are mutually exclusive and override explorer calldata. +3. If metadata is insufficient, identify the creation transaction for this chain/address from deployment records or the explorer's creation API. Obtain a creation trace when the RPC supports it; with `callTracer`, locate the relevant CREATE/CREATE2 frame, including internal factory calls. +4. Match the frame to the target deployment. Separate its input into exact linked creation bytecode and ABI-encoded arguments using the matching build artifact. Do not treat a matching address substring as the boundary. +5. Decode and re-encode the arguments against the constructor ABI; verify they account for the complete argument suffix. Compare deployer/caller and deployment records. Same-address deployments on other chains are corroborating evidence only after their creation inputs are established. +6. Set one manual override if needed, then rerun bytecode comparison. Use `deployment_from` when constructor behavior depends on `msg.sender`. Missing traces or matching artifacts leave recovery unresolved. + +For a proxy, separately establish proxy and implementation identity from deployment records and the proxy's on-chain mechanism. Checking the implementation address does not verify either contract's source or bytecode by itself. + +Distinguish CREATE from CREATE2 when reasoning about cross-chain addresses. CREATE2 binds the factory address, salt and hash of the complete initcode, including appended constructor arguments, as specified in [EIP-1014](https://eips.ethereum.org/EIPS/eip-1014). With factory and salt fixed, changing those arguments changes the derived address under the usual hash-collision assumption. Equal addresses alone do not establish the creation mechanism or inputs; identical initcode can also execute against different chain state. + +## Error-to-check map + +| Error or observation | Concrete next check | +| --- | --- | +| `missing GitHub sources for bytecode compilation` | Inspect the listed paths against commit, `relative_root` and dependency prefixes. Use `extra_sources` only for required files absent from the explorer set. | +| `Failed to infer source path for library` or `unlinked libraries` | Locate the library declaration and solc link references; correct the definition-file key and address. | +| Constructor address in both override maps | Keep the one supported by deployment evidence; match address casing to `contracts`. | +| `intrinsic gas too low` during simulation | Inspect the request gas cap and chain/RPC limits; use a supported `deployment_gas_limit` instead of disabling bytecode comparison. | +| Contract-name mismatch | Compare requested address, chain, config name and returned name; establish verification status separately. | +| Immutable mismatch | Map the exact offset/value to compiler immutable references and deployment behavior. Use a justified exact-value rule only after explaining the difference. | +| Clean source diff but bytecode mismatch | Compare compiler version, settings, EVM version, linked libraries and constructor simulation; clean source alone does not prove bytecode. | + +For flattened source, inspect how `_get_contract_from_etherscan` reconstructs `solcInput`: only basic optimizer settings are retained in that path. Missing original settings are a possible reproduction gap, not automatic justification for a wildcard. Preserve the gap in the result if it cannot be resolved. diff --git a/.claude/skills/new-config/SKILL.md b/.claude/skills/new-config/SKILL.md index f0b6aa7..523b438 100644 --- a/.claude/skills/new-config/SKILL.md +++ b/.claude/skills/new-config/SKILL.md @@ -1,181 +1,44 @@ --- name: new-config -description: Create a new diffyscan verification config file for a deployed smart contract. Use when the user wants to verify a new contract or deployment. +description: Creates or extends a Diffyscan verification config for a deployed contract or deployment. Use for adding addresses, pinning GitHub sources and dependencies, or setting up a chain in configs/. Existing failing runs belong to debug-diff; explorer adapter code belongs to add-explorer. +argument-hint: "[chain] [contract-address] [github-repo-url]" disable-model-invocation: true -argument-hint: [chain] [contract-address] [github-repo-url] --- -Create a new diffyscan config file for verifying a deployed smart contract. The user may provide some or all of these details — ask for anything missing. +Create a reproducible config covering the requested deployment. Run commands from the repository root. -## Required information +## 1. Establish the inputs -1. **Chain**: Which blockchain (ethereum, optimism, base, zksync, linea, scroll, mantle, bsc, lisk, soneium, unichain, ink, swell, megaeth, plasma, mode, etc.) -2. **Network**: mainnet or testnet (hoodi/sepolia for Ethereum) -3. **Contract address(es)**: One or more `0x`-prefixed addresses and their contract names -4. **GitHub repo**: URL, commit hash, and relative root within the repo -5. **Explorer**: hostname and token env var name +Read [configuration](../../../docs/configuration.md) and a nearby config under `configs///`. Confirm the chain, addresses and names, expected GitHub commit, source root, and explorer/RPC selection from deployment records and the user's context. Ask for information that cannot be established; do not invent a commit or shrink the requested contract set. -## Config schema +Use [add-explorer](../add-explorer/SKILL.md) if the host needs routing or API support. Check current official explorer documentation before choosing an endpoint; an old config does not establish current API availability. -Use YAML format by default (supports comments for annotating addresses). JSON if user requests it. +## 2. Write the config -The `Config` TypedDict is defined in `diffyscan/utils/custom_types.py`. Template for new configs: +Use YAML unless another format is requested. Quote addresses and hexadecimal values. Follow project naming and include: -```yaml -contracts: - "0xAddress": ContractName +- `contracts`: deployed address to explorer contract name; +- `explorer_hostname` or `explorer_hostname_env_var` (the explicit hostname takes precedence); +- `explorer_chain_id` when the chosen API needs it; +- `github_repo`: `url`, full commit SHA, and `relative_root`; +- `dependencies`: import-prefix mappings pinned to full commits, or `{}` for repository config tests. -network: mainnet # required in TypedDict but unused at runtime; include for forward-compatibility +`network` is optional descriptive metadata. Add other optional fields only when needed. Store credential environment-variable names, not secrets. The runtime loads an explorer token and `GITHUB_API_TOKEN` even for adapters that do not send the explorer token. Bytecode comparison also needs the configured RPC URL; confirm its chain. -explorer_hostname: api.etherscan.io -explorer_token_env_var: ETHERSCAN_EXPLORER_TOKEN -explorer_chain_id: 1 # activates Etherscan v2 API +Read [bytecode comparison](../../../docs/bytecode-comparison.md) before adding manual overrides. Prefer explorer constructor metadata when it describes the deployment; manual calldata is not required for every constructor. Set only one of `constructor_args` and `constructor_calldata` per address. Key libraries by their definition file; the mapping applies to every contract in the config. Use `deployment_from` for a constructor that depends on its caller and `extra_sources` for required GitHub files missing from the explorer source set. -github_repo: - url: https://github.com/org/repo - commit: "" - relative_root: "" +For proxies, distinguish proxy and implementation addresses and confirm requested scope from deployment records and on-chain state. Obtain missing calldata from the creation trace and exact creation-bytecode boundary, then ABI-decode it. An address substring is not a reliable boundary; matching addresses across chains alone do not establish identical calldata. -dependencies: {} -``` - -Note: `explorer_token_env_var`, `explorer_chain_id`, and `dependencies` are `NotRequired` in the TypedDict but should always be included in new configs. `network` is required in the TypedDict. - -YAML gotcha: addresses and hex strings MUST be quoted (`"0xabc..."`) — unquoted hex gets parsed as integers. The `load_config` function validates this and raises `ValueError` if coercion is detected. - -## Optional fields (add only when needed) - -- `dependencies` — map of import path prefix to `{url, commit, relative_root}`. Common: `@openzeppelin/contracts`, `@openzeppelin/contracts-upgradeable`, `lib/openzeppelin-contracts/contracts` -- `bytecode_comparison` — object with: - - `constructor_calldata` — raw hex calldata per address: `{"0xAddr": "0xabcd..."}` - - `constructor_args` — typed args per address: `{"0xAddr": ["0xarg1", true, 42]}` - - `libraries` — per source path: `{"contracts/lib/Foo.sol": {"Foo": "0xLibAddr"}}` -- `fail_on_bytecode_comparison_error` — defaults to `true`; when `false`, a per-contract exception (e.g. failing to fetch/verify a contract from the explorer) is logged and the run continues instead of aborting. Despite the name, an actual error inside `run_bytecode_diff` is always caught and recorded as a mismatch (`match=False`) regardless of this flag. -- `allowed_diffs` — declare expected, known diffs so the run still passes while everything else stays verified. Map of `bytecode` / `source` → `{"0xAddr": [rules]}`; each rule needs a `reason`. Prefer the **most specific** facet — for bytecode: `immutables` (exact on-chain values), `byte_ranges`, `cbor_metadata: true`, `constructor_args`/`constructor_calldata`; for source: `line_ranges` (exact hunks), `files` (whole files). `any: true` is a blanket wildcard that hides *all* future drift — use it only when a diff genuinely cannot be scoped (e.g. bytecode that can't be reproduced), and explain why in the `reason`. When a diff is uncovered, diffyscan prints a ready-to-paste snippet in the final summary; paste it and tighten the placeholder. Example: - ```yaml - allowed_diffs: - bytecode: - "0xAddr": - - reason: Factory stores its timelock as an immutable - immutables: - - offset: 304 - value: "0x000000000000000000000000468029a8..." - source: - "0xAddr": - - reason: Import paths differ between repo and explorer - line_ranges: - - file: contracts/Foo.sol - github: { start: 6, count: 5 } - explorer: { start: 6, count: 5 } - ``` -- `source_comparison` — set to `false` to skip source diffs (bytecode-only check) -- `rpc_url_env_var` — name of the env var holding the RPC URL for bytecode comparison (defaults to `REMOTE_RPC_URL`; e.g. `MANTLE_RPC_URL`, `PLASMA_RPC_URL`) -- `deployment_gas_limit` — optional gas limit for the `eth_call` deployment simulation (helps on chains where the default reverts with "intrinsic gas too low") -- `explorer_hostname_env_var` — config convention for external CI/tooling to pass the explorer hostname via env var (used for soneium, unichain). Note: diffyscan itself does NOT resolve this at runtime — `get_explorer_hostname()` only reads `explorer_hostname`. External scripts must set `explorer_hostname` before invoking diffyscan. -- `audit_url` — optional link to an audit report for documentation purposes -- `metadata` — optional object for deployment metadata (e.g. `chain_name`, `deployment_date`, `timelock_address`, `timelock_requirements`) - -## Explorer configuration - -The code dispatches to different explorer backends based on the hostname pattern (see `_get_explorer_fetcher()` in `diffyscan/utils/explorer.py`): - -### Etherscan v2 API (preferred for Etherscan-supported chains) - -Use `api.etherscan.io` as the hostname with `explorer_chain_id` to activate the v2 API (`/v2/api?chainid=`). This works for any Etherscan-supported chain with a single API token. - -```json -"explorer_hostname": "api.etherscan.io", -"explorer_token_env_var": "ETHERSCAN_EXPLORER_TOKEN", -"explorer_chain_id": 1 -``` - -Common chain IDs: Ethereum=1, Optimism=10, Base=8453, BSC=56, Linea=59144, Hoodi=560048. - -### Legacy per-chain Etherscan hostnames (still work, used in older configs) - -These are Etherscan-compatible and fall through to the default `_get_contract_from_etherscan` backend: - -| Hostname | Token env var | Notes | -|---|---|---| -| `api-optimistic.etherscan.io` | `OPTISCAN_EXPLORER_TOKEN` | Optimism mainnet | -| `api.basescan.org` | `ETHERSCAN_EXPLORER_TOKEN` | Base mainnet | -| `api.lineascan.build` | `LINEA_EXPLORER_TOKEN` | Linea mainnet (special: token is ignored by dispatcher) | -| `api.scrollscan.com` | `ETHERSCAN_EXPLORER_TOKEN` (fallback) | Scroll mainnet | -| `api.bscscan.com` | `BSCSCAN_TOKEN` | BSC mainnet | -| `api-hoodi.etherscan.io` | `ETHERSCAN_EXPLORER_TOKEN` | Hoodi testnet | -| `api-sepolia.etherscan.io` | `ETHERSCAN_EXPLORER_TOKEN` | Sepolia testnet | - -**For new configs, prefer the v2 API approach** (`api.etherscan.io` + `explorer_chain_id`) over legacy per-chain hostnames. - -### Non-Etherscan explorers (require their own hostname) - -These are detected by hostname pattern and use different API backends: - -| Type | Hostname pattern | Example hostnames | Token env var | -|---|---|---|---| -| zkSync | starts with `zksync` | `zksync2-mainnet-explorer.zksync.io` | (none needed) | -| Mantle | ends with `mantle.xyz` | `explorer.mantle.xyz`, `explorer.testnet.mantle.xyz` | (none needed) | -| Blockscout | ends with `blockscout.com`, `mode.network`, `swellnetwork.io`, `lisk.com`, `inkonchain.com`, `routescan.io`, `monadvision.com` | `blockscout.lisk.com`, `explorer.mode.network`, `explorer.swellnetwork.io`, `megaeth.blockscout.com`, `explorer.inkonchain.com` | Varies (some use API keys like `INK_API_KEY`, `MEGAETH_API_KEY`; some need none) | - -Blockscout explorers use the `/api/v2/smart-contracts/{address}` endpoint. Some Blockscout-based explorers (ink, megaeth) also set `explorer_chain_id`. - -**Note:** `api.routescan.io/v2/network/mainnet/evm/9745/etherscan` (used for Plasma) does NOT match the Blockscout dispatcher — the hostname ends with `etherscan`, not `routescan.io`. It falls through to the default Etherscan fetcher and uses `PLASMA_API_KEY`. +If constructor metadata is missing, follow [calldata recovery](../debug-diff/references/diagnostic-recipes.md#missing-or-empty-constructor-calldata) before writing an override. -### `explorer_hostname_env_var` (CI convention only) +## 3. Verify and hand off -Some configs (soneium, unichain) use `explorer_hostname_env_var` instead of `explorer_hostname`. This is a convention for external CI/tooling — diffyscan's `get_explorer_hostname()` only reads `explorer_hostname` directly. External scripts must resolve the env var and set `explorer_hostname` before invoking diffyscan. For new configs, prefer hardcoding `explorer_hostname` directly unless there's a specific CI reason not to. +Apply [validate-config](../validate-config/SKILL.md), then run the requested verification when credentials are available: -## File placement - -Save configs to `configs///.yaml` (state-mate-style layout). Kebab-case file names; the chain goes into the file name for cross-chain projects. Look at existing configs for patterns. - -## Best practices - -- **Use exact commit SHAs** — always use full 40-character commit hashes, never branch names or tags (they can change) -- **Enable bytecode comparison** — provide `constructor_calldata` or `constructor_args` for contracts with constructors so bytecode verification runs -- **Include `audit_url`** — link to the relevant audit report when available for cross-reference -- **Keep configs strict** — prefer explicit over implicit; include all fields even if optional, so verification is as thorough as possible - -## Extracting proxy constructor calldata - -Factory-created proxies (e.g. TransparentUpgradeableProxy) often need `constructor_calldata` for bytecode comparison. Three methods, in order of preference: - -**Method 1: Etherscan v2 API** (preferred) -```bash -curl -s "https://api.etherscan.io/v2/api?chainid=&module=contract&action=getsourcecode&address=&apikey=" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0].get('ConstructorArguments',''))" +```sh +uv run diffyscan path/to/config.yaml --json -E -G ``` -Factory-created contracts sometimes return empty — fall through to Method 2. - -**Method 2: debug_traceTransaction** (when Etherscan has empty args) -1. Get creation tx hash via `getcontractcreation` API -2. Trace with `{"tracer": "callTracer"}` to find CREATE/CREATE2 opcodes -3. Constructor args are appended to initcode in the CREATE2 input. Find the implementation address (padded to 32 bytes) in the hex input — everything from that point on is the constructor args. -4. Alternatively, if you know the exact initcode size (e.g. from a reference deployment on another chain), slice at that offset. - -**Method 3: Cross-chain reuse** (when traces unavailable) -CREATE2 with same factory + same init code + same salt = same address on every chain. If a proxy has the same address on two chains, its constructor calldata is identical. Copy from the chain where you already have it. - -### Verifying proxy implementations -Always verify implementations on-chain before writing the config: -```bash -cast implementation --rpc-url -``` - -### Chain-specific RPC limitations -- Some chains (Arbitrum, Mantle) do NOT support `debug_traceTransaction` on public RPCs — use Etherscan API or cross-chain reuse instead. -- Mantle's gas model causes `eth_call` to fail with "intrinsic gas too low" for large deployment simulations — use `--skip-binary-comparison`. -- Unverified contracts on a chain's explorer should be commented out in the config. - -### Dependencies: matching explorer source paths -If diffyscan fails with 404 fetching a GitHub file, the explorer source uses a path prefix that isn't covered by `dependencies`. Each key in `dependencies` maps a source path prefix → a GitHub repo. Check the explorer source file paths and add the missing prefix mapping. -## Workflow +Read [JSON output](../../../docs/json-output.md): check `status`, errors, contract coverage and enabled comparisons as well as the exit code. Caches help repeated diagnosis; omit `-E` when fresh explorer evidence is needed. For a failed run use [debug-diff](../debug-diff/SKILL.md); for explained exceptions use [allowed-diffs](../allowed-diffs/SKILL.md). -1. Gather required info from user (ask for missing pieces) -2. Look at existing configs in the same chain directory for reference patterns -3. Verify implementations on-chain with `cast implementation` -4. Extract constructor calldata (Etherscan API → trace → cross-chain reuse) -5. Create the config file with all needed dependencies -6. Run: `uv run diffyscan --yes --cache-explorer --cache-github` -7. Expected: source diffs = 0; bytecode diffs on proxies (immutable reference) is normal +Keep unverified contracts visible as unresolved scope. Do not comment them out, disable comparison, or accept proxy immutables merely to obtain a passing run. Report config paths, deployment/source evidence, checks performed and unresolved verification. diff --git a/.claude/skills/validate-config/SKILL.md b/.claude/skills/validate-config/SKILL.md index 1d6d7f7..d038594 100644 --- a/.claude/skills/validate-config/SKILL.md +++ b/.claude/skills/validate-config/SKILL.md @@ -1,130 +1,49 @@ --- name: validate-config -description: Validate a diffyscan config file for correctness before running verification. Checks schema, required fields, type correctness, and common mistakes. -argument-hint: [config-path] +description: Reviews an existing Diffyscan YAML or JSON config for load errors, runtime prerequisites, pinned sources, address mappings and broad exceptions. Use for config review or preflight validation; failed live runs belong to debug-diff and new deployment setup to new-config. +argument-hint: "[config-path]" --- -Validate the diffyscan config file at `$ARGUMENTS` (or ask for the path if not provided). +Validate the requested config without claiming static checks prove a deployment matches. Run commands from the repository root. -Read the config file using the Read tool. Use the TypedDict definitions in `diffyscan/utils/custom_types.py` as the schema reference. +## 1. Load and inspect -## Schema reference +Read the config, [configuration reference](../../../docs/configuration.md), `diffyscan/utils/common.py` and `diffyscan/utils/custom_types.py`. TypedDicts describe structure; they do not enforce it at runtime. Check the actual loader: -The `Config` TypedDict (`diffyscan/utils/custom_types.py`) defines: +```sh +uv run python - path/to/config.yaml <<'PY' +import sys +from diffyscan.utils.common import load_config +load_config(sys.argv[1]) +print("Config load passed") +PY +``` -**Required fields:** -- `contracts` — `dict[str, str]` mapping address to contract name -- `network` — `str` (declared required in TypedDict but currently unused at runtime; include it for forward-compatibility) -- `explorer_hostname` — `str` -- `github_repo` — `GithubRepo` with required keys: `url`, `commit`, `relative_root` +Then inspect what the loader does not fully validate: -**Optional fields (NotRequired):** -- `dependencies` — `dict[str, GithubRepo]` -- `explorer_token_env_var` — `str` -- `explorer_chain_id` — `int` -- `bytecode_comparison` — `BinaryConfig` -- `fail_on_bytecode_comparison_error` — `bool` -- `source_comparison` — `bool` +- Nonempty `contracts`, string names and valid 20-byte hexadecimal addresses. Quote YAML hex keys and values, including nested overrides. +- `github_repo` and each dependency have `url`, a full commit SHA and `relative_root`. Dependency prefixes match published source paths. Repository config tests require `dependencies`, including when empty. +- An explicit explorer hostname or `explorer_hostname_env_var`; the runtime resolves the latter when the explicit hostname is absent. `network`, `audit_url` and `metadata` are optional descriptive fields. +- `get_explorer_chain_id` in `diffyscan/utils/explorer.py` converts the configured value with `int()`. Check that it identifies the intended chain; conversion alone does not validate a chain ID. Confirm the API and RPC agree; the CLI does not compare their chain IDs. +- Credential variable names and availability without printing values. Explorer token fallback is supported; an omitted token variable name is not inherently invalid. A token value is loaded even for adapters that do not send it. +- Boolean flags and enabled comparisons. `source_comparison: false` combined with `--skip-binary-comparison` is rejected. -**Additional fields found in real configs but not in the TypedDict:** -- `explorer_hostname_env_var` — `str` (CI convention only — diffyscan does NOT resolve this at runtime; external tooling must set `explorer_hostname` before invoking) -- `audit_url` — `str` -- `metadata` — `dict` (free-form project metadata) +## 2. Review overrides and exceptions -The `BinaryConfig` TypedDict has all-optional fields: -- `constructor_calldata` — `dict[str, str]` mapping address to raw hex calldata -- `constructor_args` — `dict[str, list]` mapping address to a list of ABI-encodable arguments -- `libraries` — `dict[str, dict[str, str]]` mapping source path to `{LibraryName: "0xAddress"}` +Read [bytecode comparison](../../../docs/bytecode-comparison.md). Cross-check per-contract keys against `contracts`; flag unused entries. Preserve exact address spelling for `constructor_args` and `constructor_calldata`: their runtime lookup is case-sensitive, unlike allowed-diff rules. Check calldata hex, argument list shapes, mutually exclusive constructor overrides, `deployment_from` addresses and `extra_sources` paths. Library keys identify the definition file and apply to all contracts in the config. -## Checks to perform +`load_config` validates `allowed_diffs` through `diffyscan/utils/allowed_diffs.py`. Schema validity does not justify a rule: inspect reason and scope. Use [allowed-diffs](../allowed-diffs/SKILL.md) when tightening or adding exceptions. `fail_on_bytecode_comparison_error: false` can let an outer contract error continue, but caught bytecode errors still produce failed results. -### 1. Required fields +For changes under `configs/`, run: -- `contracts` must be present and be a non-empty dict -- `explorer_hostname` must be a string (or alternatively `explorer_hostname_env_var` must be present; real configs use one or both -- see `tests/test_configs.py` line 32) -- `github_repo` must be present and contain all three keys: `url`, `commit`, `relative_root` -- `network` is declared required in the TypedDict. Warn if missing, noting it is not used at runtime today but may be in the future +```sh +uv run pytest -q tests/test_configs.py tests/test_no_wildcard_regression.py +``` -### 2. YAML hex coercion (what the codebase actually validates) +These tests inspect repository configs, not arbitrary files outside `configs/`. Loader success alone does not cover all schema fields, source availability, compiler reproduction or on-chain correctness. -The function `_validate_yaml_hex_keys` in `diffyscan/utils/common.py` checks YAML configs for hex values that PyYAML silently coerced from strings to integers. It raises `ValueError` if any are found. Specifically it checks: +## 3. Report -- **`contracts` keys** (address) -- raises if parsed as `int` -- **`contracts` values** (contract name) -- raises if parsed as `int` -- **`bytecode_comparison.constructor_args` keys** -- raises if parsed as `int` -- **`bytecode_comparison.constructor_calldata` keys** -- raises if parsed as `int` -- **`bytecode_comparison.libraries` values** (the library address strings) -- raises if parsed as `int` +List concrete errors with config keys and locations, then evidence gaps or recommendations. Separate loader success, repository tests and live verification. If live verification was requested, run it with `--json` and inspect status, errors and coverage using [JSON output](../../../docs/json-output.md); otherwise report the static verdict and its limits. Edit the config only when the task includes fixes. -This validation only runs for YAML files, not JSON. It only detects `int` coercion; it does NOT validate address format (0x prefix, 42 chars, valid hex, checksum). - -### 3. Address format (best-practice recommendation only) - -The codebase does NOT validate address format at config load time. There is no runtime check for 0x prefix, 42-character length, or hex validity on addresses in the config. Addresses are passed directly to the explorer API and RPC node. - -However, the test suite (`tests/test_configs.py:test_contract_addresses_format`) asserts all `contracts` keys start with `0x` and are 42 characters. Recommend the same for any address in the config: -- Contract addresses in `contracts` keys -- Addresses in `bytecode_comparison.constructor_calldata` keys -- Addresses in `bytecode_comparison.constructor_args` keys -- Library addresses in `bytecode_comparison.libraries` values - -### 4. Explorer configuration - -- If `explorer_token_env_var` is missing, the runtime warns and falls back to `ETHERSCAN_EXPLORER_TOKEN` (see `_load_explorer_token` in `diffyscan/diffyscan.py`). Warn if absent. -- `explorer_chain_id` is optional; the runtime does not warn if missing (retrieved with `warn_if_missing=False`) -- `explorer_hostname` is retrieved with `warn_if_missing=True`; if absent the runtime logs a warning - -### 5. GitHub repo fields - -- `github_repo.url` should look like a GitHub URL -- `github_repo.commit` should ideally be a full 40-character SHA hex string (warn if short or non-hex) -- `github_repo.relative_root` can be an empty string (commonly is for root-level repos) - -### 6. Dependencies - -- Each dependency value must have `url`, `commit`, `relative_root` (same `GithubRepo` shape) -- Dependency keys should match import path prefixes used in Solidity sources (e.g. `@openzeppelin/contracts`, `lib/openzeppelin-contracts-upgradeable/contracts`) -- The runtime resolves dependencies by checking if a source file path starts with `"{dep_name}/"` (see `resolve_dep` in `diffyscan/utils/github.py`) - -### 7. Bytecode comparison - -- `constructor_calldata` values should be hex strings (the runtime strips `0x` prefix via `normalize_calldata` and validates hex content) -- `constructor_args` values must be lists (arrays of ABI-encodable values) -- A contract address must NOT appear in both `constructor_calldata` and `constructor_args` -- the runtime raises `CalldataError` if it does (see `get_calldata` in `diffyscan/utils/calldata.py`) -- `libraries` maps Solidity source file paths to `{LibraryName: "0xAddress"}` dicts - -### 8. Cross-reference checks - -What the runtime actually does: -- Addresses in `bytecode_comparison.constructor_calldata` and `constructor_args` are looked up by contract address at runtime -- if a contract has a constructor but its address is not in either dict and the explorer has no constructor arguments, the runtime raises `CalldataError` -- Contracts listed in `contracts` that have no corresponding entry in `bytecode_comparison` will still work -- they fall back to explorer-provided constructor arguments -- There is no compile-time cross-reference validation in the codebase; all checks happen at runtime - -Recommended cross-reference warnings: -- Warn if an address appears in `constructor_calldata` or `constructor_args` but not in `contracts` (it would be unused) -- Warn if a contract is in both `constructor_calldata` and `constructor_args` (runtime error) - -### 9. Allowed diffs - -`allowed_diffs` (optional) declares expected diffs. It is validated at config-load time by `validate_allowed_diffs_config` in `diffyscan/utils/allowed_diffs.py`, which raises `ValueError` on: -- top-level not a mapping, or a key other than `bytecode` / `source` -- an address not present in `contracts` (case-insensitive; a casing mismatch logs a warning) -- an empty rule list, or a rule missing a non-empty `reason` -- `any` combined with any other facet, or `any` not literally `true` -- a rule with no facet and no `any` -- bytecode: both `constructor_args` and `constructor_calldata`; `cbor_metadata` not `true`; duplicate/negative immutable `offset`; non-positive `byte_ranges.length`; invalid/odd-length hex in `value`/`constructor_calldata` -- source: `line_ranges` span with `start < 1` or `count < 0`; empty `files`; unknown keys - -Beyond schema validity, **flag every `any: true` rule as a smell**: it suppresses all diffs for that contract and hides future drift. Recommend tightening it to a granular facet (`immutables`, `byte_ranges`, `cbor_metadata`, `line_ranges`, `files`) using the suggestion diffyscan prints in its final summary. `any: true` is acceptable only when a diff genuinely cannot be scoped (e.g. unreproducible bytecode), and the `reason` should say so. Note: `tests/test_no_wildcard_regression.py` fails CI on any new wildcard not listed in its `KNOWN_WILDCARDS`. - -### 10. Optional flags - -- `fail_on_bytecode_comparison_error` defaults to `true` if absent -- `source_comparison` defaults to `true` if absent; set to `false` to skip source diffs - -## Output - -Report issues in two categories: -- **Errors** (must fix): missing required fields, type mismatches, YAML hex coercion, duplicate entries in both `constructor_calldata` and `constructor_args` -- **Warnings** (should review): missing `explorer_token_env_var`, short commit SHA, addresses not matching 0x/42-char format, missing `network`, unused bytecode_comparison entries - -If the config looks good, confirm it passes validation. +Treat placeholder-looking addresses and commits as unverified inputs, not proof that a fetch will fail. An empty `dependencies` map is not an error without evidence of unresolved imports: those sources may belong to the primary repository. Report supported defaults as defaults rather than missing-field findings, and keep a review-only answer focused on findings instead of rewriting a valid config. diff --git a/CLAUDE.md b/CLAUDE.md index fbdb716..2542a92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,10 +34,20 @@ uv run black diffyscan/ tests/ # Run pre-commit hooks uv run pre-commit run --all-files -# Install git hooks (pre-commit + commit-msg via gitlint) +# Install git hooks (pre-commit + commit-msg) uv run pre-commit install --hook-type pre-commit --hook-type commit-msg ``` +## Task workflows + +Use the relevant repository skill; read linked references only when needed: + +- [new-config](.claude/skills/new-config/SKILL.md): create or extend a deployment config. +- [validate-config](.claude/skills/validate-config/SKILL.md): review config structure and runtime prerequisites. +- [debug-diff](.claude/skills/debug-diff/SKILL.md): diagnose failed or incomplete verification. +- [allowed-diffs](.claude/skills/allowed-diffs/SKILL.md): justify and scope expected differences or tighten wildcards. +- [add-explorer](.claude/skills/add-explorer/SKILL.md): extend explorer routing or response parsing. + ## Architecture Entry point: `diffyscan/diffyscan.py:main` — parses CLI args, loads config (JSON or YAML), then runs source diff and bytecode diff for each contract. @@ -57,7 +67,8 @@ Entry point: `diffyscan/diffyscan.py:main` — parses CLI args, loads config (JS - **encoder.py** — ABI encoding for constructor arguments (address, bool, int/uint, bytes, tuples, arrays) - **calldata.py** — resolves constructor calldata from config or explorer metadata - **node_handler.py** — RPC calls: `eth_getCode`, `eth_chainId`, `eth_call` -- **common.py** — config loading (with YAML hex address validation), HTTP helpers, caching with SHA256 validation +- **common.py** — config loading (with YAML hex address validation), caching with SHA256 validation +- **http_client.py** — shared HTTP requests, User-Agent and error handling - **custom_types.py** — TypedDict definitions: `Config`, `BinaryConfig`, `ExplorerContract`, `GithubRepo` - **custom_exceptions.py** — exception hierarchy; `ExceptionHandler` controls fail-or-log behavior @@ -87,5 +98,5 @@ Supports loading from `.env` (see `.env.example`), or set directly: `GITHUB_API_ ## Code style - Formatter: **black** (enforced via pre-commit) -- Commit messages: validated by **gitlint** +- Commit messages: validated by **conventional-pre-commit** - Python >=3.11 From 2d01b85792feae5256f2b721ce282ef66fdad866 Mon Sep 17 00:00:00 2001 From: Yuri T Date: Mon, 7 Sep 2026 18:10:00 +0100 Subject: [PATCH 2/2] docs: clarify verification skill limits after review --- .claude/skills/allowed-diffs/SKILL.md | 8 +++++++- .claude/skills/debug-diff/SKILL.md | 2 +- .../skills/debug-diff/references/diagnostic-recipes.md | 2 +- .claude/skills/new-config/SKILL.md | 2 +- .claude/skills/validate-config/SKILL.md | 4 +++- CLAUDE.md | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.claude/skills/allowed-diffs/SKILL.md b/.claude/skills/allowed-diffs/SKILL.md index 794eafb..4aabd8e 100644 --- a/.claude/skills/allowed-diffs/SKILL.md +++ b/.claude/skills/allowed-diffs/SKILL.md @@ -5,17 +5,21 @@ description: Adds, reviews or tightens Diffyscan allowed_diffs rules for explain Encode an explained difference without accepting unrelated drift. Run commands from the repository root. +For review-only requests, inspect existing evidence and report findings or proposed rules; edit configs and run live diagnostics only when the task includes those actions. + ## 1. Establish evidence Read the target config, [bytecode comparison](../../../docs/bytecode-comparison.md) and [JSON output](../../../docs/json-output.md). Confirm chain, contract, pinned source and why the difference is intended. If unknown, use [debug-diff](../debug-diff/SKILL.md) before changing policy. Use `suggested_rule` and actual diff evidence as a starting point. Suggestions describe observations; they do not justify accepting them. To replace a wildcard, remove only that rule for a diagnostic run or use a temporary config copy, then inspect uncovered differences. Preserve other rules and contract scope. +A `files` suggestion can come from a source file missing on GitHub. Resolve the missing file's provenance, path or dependency before accepting the suggestion; it would otherwise allow the entire source omission. + ## 2. Choose a rule Inspect `evaluate_source_rules`, `evaluate_bytecode_rules` and matchers in `diffyscan/utils/allowed_diffs.py` when coverage is unclear. Rules are alternatives: one rule must cover the differences; separate entries do not accumulate coverage. Combine necessary facets within one rule. -- Prefer source `line_ranges` for exact hunks. Coordinates are 1-based; `count: 0` represents insertion/deletion. `files` accepts future changes throughout named files. +- Prefer source `line_ranges` for exact hunk coordinates. Coordinates are 1-based; `count: 0` represents insertion/deletion. These rules do not pin line contents: different edits at the same coordinates are also accepted. `files` accepts future changes throughout named files. - Prefer bytecode `immutables` with exact on-chain values at compiler-derived offsets when those values explain the difference. `byte_ranges` constrains offsets and lengths but does not pin values there. - Add `cbor_metadata: true` for explained metadata differences, combined with other necessary facets in the same rule. - Use `constructor_args` or `constructor_calldata` for an explained alternate simulation, respecting mutual exclusion. Verify the resulting runtime; an override alone does not prove a match. @@ -25,6 +29,8 @@ Every rule needs a concrete `reason` describing deployment evidence and intended The exclusivity of `any` applies inside one `allowed_diffs` rule. A constructor override in the separate `bytecode_comparison` section can coexist with an `any` rule; that combination is broad policy, not a schema conflict. +Constructor overrides inside rules are evaluated only after the base simulation succeeds and still differs. They cannot recover missing base calldata or a reverting base constructor. Evidence-backed overrides in `bytecode_comparison` supply the base simulation inputs. + ## 3. Verify the result Apply [validate-config](../validate-config/SKILL.md), then: diff --git a/.claude/skills/debug-diff/SKILL.md b/.claude/skills/debug-diff/SKILL.md index 30c23a7..fa78f30 100644 --- a/.claude/skills/debug-diff/SKILL.md +++ b/.claude/skills/debug-diff/SKILL.md @@ -29,7 +29,7 @@ Read [configuration](../../../docs/configuration.md) for prerequisites and [byte | Evidence | Check next | | --- | --- | | Missing source or GitHub 404 | Commit, `relative_root` and import-prefix resolution in `diffyscan/utils/github.py`; distinguish a wrong path from a missing dependency. | -| Source hunks | Inspect report HTML and actual changes. Confirm deployment provenance before changing the pinned commit. Use `--support-brownie` only for flattened import-path resolution. | +| Source hunks | Inspect report HTML and actual changes. Confirm deployment provenance before changing the pinned commit. `--support-brownie` enables recursive filename lookup that can select the first same-named file in another directory; confirm the resolved path before trusting the comparison. | | Compilation error | `run_bytecode_diff` in `diffyscan/diffyscan.py`, compiler/settings, dependencies, `extra_sources` and library definition paths. | | Calldata or simulation error | `diffyscan/utils/calldata.py`, explorer metadata, constructor ABI, `deployment_from`, RPC state and `deployment_gas_limit`. Recover calldata from exact creation input and ABI, not an address substring or cross-chain address match. | | Bytecode differences | `analyze_bytecode_diff` in `diffyscan/utils/binary_verifier.py` and evaluation in `diffyscan/utils/allowed_diffs.py`; inspect uncovered ranges, metadata, runtime length and immutable values. | diff --git a/.claude/skills/debug-diff/references/diagnostic-recipes.md b/.claude/skills/debug-diff/references/diagnostic-recipes.md index 22945ea..d582893 100644 --- a/.claude/skills/debug-diff/references/diagnostic-recipes.md +++ b/.claude/skills/debug-diff/references/diagnostic-recipes.md @@ -33,7 +33,7 @@ Distinguish CREATE from CREATE2 when reasoning about cross-chain addresses. CREA | `missing GitHub sources for bytecode compilation` | Inspect the listed paths against commit, `relative_root` and dependency prefixes. Use `extra_sources` only for required files absent from the explorer set. | | `Failed to infer source path for library` or `unlinked libraries` | Locate the library declaration and solc link references; correct the definition-file key and address. | | Constructor address in both override maps | Keep the one supported by deployment evidence; match address casing to `contracts`. | -| `intrinsic gas too low` during simulation | Inspect the request gas cap and chain/RPC limits; use a supported `deployment_gas_limit` instead of disabling bytecode comparison. | +| `intrinsic gas too low` during simulation | Inspect request gas and chain/RPC limits. `deployment_gas_limit` is a top-level config key; its default is `2**24`. Do not raise it blindly: strict nodes can reject values above their cap. Check constructor inputs and RPC state before changing it. | | Contract-name mismatch | Compare requested address, chain, config name and returned name; establish verification status separately. | | Immutable mismatch | Map the exact offset/value to compiler immutable references and deployment behavior. Use a justified exact-value rule only after explaining the difference. | | Clean source diff but bytecode mismatch | Compare compiler version, settings, EVM version, linked libraries and constructor simulation; clean source alone does not prove bytecode. | diff --git a/.claude/skills/new-config/SKILL.md b/.claude/skills/new-config/SKILL.md index 523b438..5be127b 100644 --- a/.claude/skills/new-config/SKILL.md +++ b/.claude/skills/new-config/SKILL.md @@ -23,7 +23,7 @@ Use YAML unless another format is requested. Quote addresses and hexadecimal val - `github_repo`: `url`, full commit SHA, and `relative_root`; - `dependencies`: import-prefix mappings pinned to full commits, or `{}` for repository config tests. -`network` is optional descriptive metadata. Add other optional fields only when needed. Store credential environment-variable names, not secrets. The runtime loads an explorer token and `GITHUB_API_TOKEN` even for adapters that do not send the explorer token. Bytecode comparison also needs the configured RPC URL; confirm its chain. +`network` is optional descriptive metadata. Add other optional fields only when needed. Store credential environment-variable names, not secrets. Set `explorer_token_env_var` to an available token variable, or confirm the `ETHERSCAN_EXPLORER_TOKEN` fallback is available. The runtime loads an explorer token and `GITHUB_API_TOKEN` even for adapters that do not send the explorer token. Bytecode comparison also needs the configured RPC URL; confirm its chain. Read [bytecode comparison](../../../docs/bytecode-comparison.md) before adding manual overrides. Prefer explorer constructor metadata when it describes the deployment; manual calldata is not required for every constructor. Set only one of `constructor_args` and `constructor_calldata` per address. Key libraries by their definition file; the mapping applies to every contract in the config. Use `deployment_from` for a constructor that depends on its caller and `extra_sources` for required GitHub files missing from the explorer source set. diff --git a/.claude/skills/validate-config/SKILL.md b/.claude/skills/validate-config/SKILL.md index d038594..a088c06 100644 --- a/.claude/skills/validate-config/SKILL.md +++ b/.claude/skills/validate-config/SKILL.md @@ -32,7 +32,9 @@ Then inspect what the loader does not fully validate: Read [bytecode comparison](../../../docs/bytecode-comparison.md). Cross-check per-contract keys against `contracts`; flag unused entries. Preserve exact address spelling for `constructor_args` and `constructor_calldata`: their runtime lookup is case-sensitive, unlike allowed-diff rules. Check calldata hex, argument list shapes, mutually exclusive constructor overrides, `deployment_from` addresses and `extra_sources` paths. Library keys identify the definition file and apply to all contracts in the config. -`load_config` validates `allowed_diffs` through `diffyscan/utils/allowed_diffs.py`. Schema validity does not justify a rule: inspect reason and scope. Use [allowed-diffs](../allowed-diffs/SKILL.md) when tightening or adding exceptions. `fail_on_bytecode_comparison_error: false` can let an outer contract error continue, but caught bytecode errors still produce failed results. +`load_config` validates `allowed_diffs` through `diffyscan/utils/allowed_diffs.py`. Schema validity does not justify a rule: inspect reason and scope. Use [allowed-diffs](../allowed-diffs/SKILL.md) when tightening or adding exceptions. + +With bytecode comparison enabled, `fail_on_bytecode_comparison_error: false` lets outer per-contract errors continue, including explorer/source errors. With `--skip-binary-comparison`, that config flag is not applied. Caught bytecode errors produce failed results, except that a bytecode `any: true` rule marks `DeploymentSimulationError` as allowed. For changes under `configs/`, run: diff --git a/CLAUDE.md b/CLAUDE.md index 2542a92..4e14416 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ Entry point: `diffyscan/diffyscan.py:main` — parses CLI args, loads config (JS - **explorer.py** — largest module; fetches/parses contracts from blockchain explorers, handles multi-chain API differences, library detection, EVM version normalization, solc compilation - **github.py** — GitHub API integration, file fetching with caching, dependency resolution (e.g. `@openzeppelin/contracts-v4.4`) -- **binary_verifier.py** — EVM bytecode parsing into instructions, metadata trimming, deep comparison with immutable region exclusion +- **binary_verifier.py** — EVM bytecode parsing into instructions, metadata separation, and difference analysis with immutable region annotations - **compiler.py** — solc binary download (platform-aware), SHA256 verification, compilation via standard JSON - **encoder.py** — ABI encoding for constructor arguments (address, bool, int/uint, bytes, tuples, arrays) - **calldata.py** — resolves constructor calldata from config or explorer metadata