diff --git a/.claude/skills/debug-diff/SKILL.md b/.claude/skills/debug-diff/SKILL.md index c58403e9..4fff3348 100644 --- a/.claude/skills/debug-diff/SKILL.md +++ b/.claude/skills/debug-diff/SKILL.md @@ -87,7 +87,7 @@ uv run diffyscan --yes --cache-explorer --cache-github - `--cache-github` (`-G`) reuses cached GitHub file fetches - `--support-brownie` enables recursive retrieval for brownie-verified contracts with flattened import paths -To accept known diffs, prefer config `allowed_diffs` rules over CLI flags. The `--allow-source-diff 0xAddr` / `--allow-bytecode-diff 0xAddr` flags still work but are **deprecated** shorthands for `any: true` (a blanket wildcard that hides all future drift). 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. +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. ## Known limitations diff --git a/.claude/skills/new-config/SKILL.md b/.claude/skills/new-config/SKILL.md index 65730096..027bdf37 100644 --- a/.claude/skills/new-config/SKILL.md +++ b/.claude/skills/new-config/SKILL.md @@ -50,7 +50,6 @@ YAML gotcha: addresses and hex strings MUST be quoted (`"0xabc..."`) — unquote - `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"}}` - - `hardhat_config_name` — (deprecated) name of a hardhat config file - `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 diff --git a/.claude/skills/validate-config/SKILL.md b/.claude/skills/validate-config/SKILL.md index e194ae92..1d6d7f7d 100644 --- a/.claude/skills/validate-config/SKILL.md +++ b/.claude/skills/validate-config/SKILL.md @@ -32,7 +32,6 @@ The `Config` TypedDict (`diffyscan/utils/custom_types.py`) defines: - `metadata` — `dict` (free-form project metadata) The `BinaryConfig` TypedDict has all-optional fields: -- `hardhat_config_name` — `str` (deprecated, ignored at runtime) - `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"}` @@ -92,7 +91,6 @@ However, the test suite (`tests/test_configs.py:test_contract_addresses_format`) - `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 -- `hardhat_config_name` is deprecated and ignored at runtime (a warning is logged) ### 8. Cross-reference checks @@ -127,6 +125,6 @@ Beyond schema validity, **flag every `any: true` rule as a smell**: it suppresse 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`, deprecated `hardhat_config_name`, unused bytecode_comparison entries +- **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. diff --git a/CLAUDE.md b/CLAUDE.md index 65f5be7a..9eef29cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,7 @@ Entry point: `diffyscan/diffyscan.py:main` — parses CLI args, loads config (JS Configs live in `config_samples/` organized by chain (ethereum, optimism, zksync, etc.): ``` contracts: { "0xaddr": "ContractName" } -network: "mainnet" # required in TypedDict, unused at runtime +network: "mainnet" # optional legacy label, unused at runtime explorer_hostname: "api.etherscan.io" explorer_token_env_var: "ETHERSCAN_EXPLORER_TOKEN" github_repo: { url, commit, relative_root } @@ -72,10 +72,9 @@ bytecode_comparison: { constructor_calldata, constructor_args, libraries, deploy allowed_diffs: { bytecode: {...}, source: {...} } # optional; see below ``` +`allowed_diffs` declares known/expected diffs per contract so a run still passes while everything else stays verified (validated by `diffyscan/utils/allowed_diffs.py`). Each rule needs a `reason`. **Prefer the tightest facet** — bytecode: `immutables`, `byte_ranges`, `cbor_metadata`, `constructor_args`/`constructor_calldata`; source: `line_ranges`, `files`. `any: true` is a blanket wildcard that hides all future drift — avoid it unless a diff genuinely can't be scoped, and say why in the `reason`. `tests/test_no_wildcard_regression.py` guards against new wildcards. `bytecode_comparison.extra_sources` (`{ "0xaddr": ["path/to/File.sol"] }`, optional) names additional source files to fetch from the configured GitHub repo/commit and add to the compilation — for contracts whose explorer-verified source set omits a file the pinned GitHub source imports (e.g. a newly-added interface). The files are fetched the same way as all others (honoring `dependencies`), so verification stays honest. -`allowed_diffs` declares known/expected diffs per contract so a run still passes while everything else stays verified (validated by `diffyscan/utils/allowed_diffs.py`). Each rule needs a `reason`. **Prefer the tightest facet** — bytecode: `immutables`, `byte_ranges`, `cbor_metadata`, `constructor_args`/`constructor_calldata`; source: `line_ranges`, `files`. `any: true` is a blanket wildcard that hides all future drift — avoid it unless a diff genuinely can't be scoped, and say why in the `reason`. The deprecated `--allow-source-diff`/`--allow-bytecode-diff` CLI flags are just shorthands for `any: true`. `tests/test_no_wildcard_regression.py` guards against new wildcards. - ### Environment variables Supports loading from `.env` (see `.env.example`), or set directly: `GITHUB_API_TOKEN`, `ETHERSCAN_EXPLORER_TOKEN`, `REMOTE_RPC_URL` (for bytecode comparison). diff --git a/README.md b/README.md index 06fdec04..80019faa 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,6 @@ Recommended workflow to tighten a wildcard: 2. Copy the `allowed_diffs` snippet it prints for the uncovered diff. 3. Paste it into the config, replace the placeholder `reason` with the real explanation, and re-run to confirm it now passes. -`--allow-bytecode-diff` and `--allow-source-diff` still work, but they are **deprecated** shorthands for `any: true` (diffyscan warns when they are used). Move those rules into the config file so the summary suggestions can help you tighten them over time. - Start the script ```bash diff --git a/diffyscan/diffyscan.py b/diffyscan/diffyscan.py index 71e2f68c..d96d7cfb 100644 --- a/diffyscan/diffyscan.py +++ b/diffyscan/diffyscan.py @@ -37,7 +37,6 @@ compile_contract_from_explorer, get_contract_from_explorer, get_explorer_chain_id, - get_explorer_hostname, get_solc_sources, merge_libraries, parse_compiled_contract, @@ -331,12 +330,12 @@ def run_source_diff( source_files = get_solc_sources(contract_code["solcInput"]) standard_json_format = is_standard_json_contract(source_files) - explorer_hostname = get_explorer_hostname(config) + explorer_hostname = config.get("explorer_hostname") explorer_chain_id = get_explorer_chain_id(config) logger.divider() logger.okay("Contract", contract_address_from_config) logger.okay("Blockchain explorer Hostname", explorer_hostname) - if explorer_chain_id: + if explorer_chain_id is not None: logger.okay("Blockchain explorer Chain ID", explorer_chain_id) else: logger.warn("Blockchain explorer Chain ID isn't set") @@ -589,31 +588,12 @@ def _log_explorer_bytecode_metadata( ) -def _warn_deprecated_hardhat_settings( - config: dict, - hardhat_config_path: str | None, -) -> None: - if hardhat_config_path: - logger.warn("--hardhat-path is deprecated and ignored") - - bytecode_comparison = config.get("bytecode_comparison", {}) - if isinstance(bytecode_comparison, dict) and bytecode_comparison.get( - "hardhat_config_name" - ): - logger.warn( - 'Config key "bytecode_comparison.hardhat_config_name" is deprecated and ignored' - ) - - def process_config( path: str, - hardhat_config_path: str | None, recursive_parsing: bool, enable_binary_comparison: bool, cache_explorer: bool, cache_github: bool, - cli_allowed_source_diffs: list[str] | None, - cli_allowed_bytecode_diffs: list[str] | None, skip_user_input: bool = False, contract_filter: list[str] | None = None, ): @@ -621,12 +601,7 @@ def process_config( logger.info(f"Loading config {path}...") config: dict = load_config(path) # type: ignore[assignment] - _warn_deprecated_hardhat_settings(config, hardhat_config_path) - effective_allowed_diffs = build_effective_allowed_diffs( - config, - cli_allowed_source_diffs, - cli_allowed_bytecode_diffs, - ) + effective_allowed_diffs = build_effective_allowed_diffs(config) explorer_token = _load_explorer_token(config) github_api_token = load_env("GITHUB_API_TOKEN", masked=True, required=True) @@ -652,8 +627,11 @@ def process_config( remote_chain_id = get_chain_id(remote_rpc_url) logger.okay("Remote chain ID", remote_chain_id) - explorer_hostname = get_explorer_hostname(config) + explorer_hostname = config.get("explorer_hostname") + if explorer_hostname is None: + logger.warn('Failed to find "explorer_hostname" in the config') assert explorer_hostname is not None, "explorer_hostname is required" + explorer_chain_id = get_explorer_chain_id(config) # Apply contract filter if specified filter_set = ( @@ -670,7 +648,7 @@ def process_config( explorer_hostname, contract_address, contract_name, - get_explorer_chain_id(config), + explorer_chain_id, cache_explorer, ) @@ -769,11 +747,6 @@ def parse_arguments() -> argparse.Namespace: default=None, help="Path to config or directory with configs", ) - parser.add_argument( - "--hardhat-path", - default=None, - help=argparse.SUPPRESS, - ) parser.add_argument( "--yes", "-Y", @@ -803,20 +776,6 @@ def parse_arguments() -> argparse.Namespace: help="Cache files retrieved from GitHub to avoid re-fetching on repeated runs", action="store_true", ) - parser.add_argument( - "--allow-source-diff", - dest="allow_source_diff", - action="append", - default=[], - help="DEPRECATED shorthand for an allowed_diffs source 'any: true' rule; prefer config allowed_diffs. Allow source diffs for a specific contract address (0x...). Can be passed multiple times.", - ) - parser.add_argument( - "--allow-bytecode-diff", - dest="allow_bytecode_diff", - action="append", - default=[], - help="DEPRECATED shorthand for an allowed_diffs bytecode 'any: true' rule; prefer config allowed_diffs. Allow bytecode diffs for a specific contract address (0x...). Can be passed multiple times.", - ) parser.add_argument( "--log-level", choices=["info", "okay", "warn", "error"], @@ -917,13 +876,10 @@ def main() -> None: for config_path in config_paths: result = process_config( config_path, - args.hardhat_path, args.support_brownie, enable_binary_comparison, args.cache_explorer, args.cache_github, - args.allow_source_diff, - args.allow_bytecode_diff, args.yes, args.contract_filter, ) diff --git a/diffyscan/utils/allowed_diffs.py b/diffyscan/utils/allowed_diffs.py index c2d24a86..9b606bdd 100644 --- a/diffyscan/utils/allowed_diffs.py +++ b/diffyscan/utils/allowed_diffs.py @@ -77,11 +77,7 @@ def validate_allowed_diffs_config(config: dict, path: str) -> None: _validate_source_rule_entry(entry, scope) -def build_effective_allowed_diffs( - config: dict, - cli_source_addrs: list[str] | None = None, - cli_bytecode_addrs: list[str] | None = None, -) -> dict: +def build_effective_allowed_diffs(config: dict) -> dict: allowed_diffs = config.get("allowed_diffs") or {} effective: dict[str, dict[str, list[dict]]] = {"source": {}, "bytecode": {}} @@ -96,31 +92,6 @@ def build_effective_allowed_diffs( _normalize_rule_entry(diff_kind, entry) for entry in entries ] - for diff_kind, addresses in ( - ("source", cli_source_addrs or []), - ("bytecode", cli_bytecode_addrs or []), - ): - if not addresses: - continue - logger.warn( - f"--allow-{diff_kind}-diff is deprecated", - "move these rules into config.allowed_diffs", - ) - for address in addresses: - normalized_address = address.lower() - if normalized_address in effective[diff_kind]: - logger.warn( - f"Ignoring CLI --allow-{diff_kind}-diff for {address}", - "config.allowed_diffs takes precedence", - ) - continue - effective[diff_kind][normalized_address] = [ - { - "reason": f"CLI allow-{diff_kind}-diff", - "any": True, - } - ] - return effective diff --git a/diffyscan/utils/binary_verifier.py b/diffyscan/utils/binary_verifier.py index b96e9750..fca05d94 100644 --- a/diffyscan/utils/binary_verifier.py +++ b/diffyscan/utils/binary_verifier.py @@ -1,6 +1,5 @@ from .logger import logger, bgYellow, bgRed, bgGreen, red, green, to_hex from .constants import OPCODES, PUSH0, PUSH32 -from .custom_exceptions import BinVerifierError def format_bytecode(bytecode: str) -> str: @@ -199,49 +198,6 @@ def log_bytecode_diff_analysis(analysis: dict) -> None: ) -def deep_match_bytecode( - actual_bytecode: str, - expected_bytecode: str, - immutables: dict, -) -> bool: - """ - Backward-compatible wrapper around bytecode analysis. - - Metadata-only differences are still ignored here. The CLI now uses the - structured analysis result directly for granular allowlists. - """ - analysis = analyze_bytecode_diff(actual_bytecode, expected_bytecode, immutables) - - if ( - not analysis["runtime_mismatch_ranges"] - and not analysis["string_literal_mismatch"] - and not analysis["length_mismatch"] - ): - logger.okay("Bytecodes match (after trimming metadata and string literals)") - return True - - log_bytecode_diff_analysis(analysis) - - if analysis["length_mismatch"]: - raise BinVerifierError( - "Bytecodes have different length after trimming metadata and string literals" - ) - - if analysis["string_literal_mismatch"]: - raise BinVerifierError("Bytecodes have different string literals") - - if any( - not range_info["immutable"] - for range_info in analysis["runtime_mismatch_ranges"] - ): - raise BinVerifierError( - "Bytecodes have differences not on the immutable reference position" - ) - - logger.warn("Bytecodes have differences only on the immutable reference position") - return False - - def _compute_runtime_mismatch_ranges( local_runtime: str, remote_runtime: str, diff --git a/diffyscan/utils/custom_exceptions.py b/diffyscan/utils/custom_exceptions.py index 5bb558ff..00747997 100644 --- a/diffyscan/utils/custom_exceptions.py +++ b/diffyscan/utils/custom_exceptions.py @@ -7,7 +7,6 @@ class BaseCustomException(Exception): def __init__(self, reason: str): message = f"{self.prefix}: {reason}" if self.prefix else reason super().__init__(message) - self.message = message class CompileError(BaseCustomException): @@ -34,10 +33,6 @@ class ExplorerError(BaseCustomException): prefix = "Failed to communicate with a remote resource" -class BinVerifierError(BaseCustomException): - prefix = "Failed in binary comparison" - - class ExceptionHandler: raise_exception = True diff --git a/diffyscan/utils/custom_types.py b/diffyscan/utils/custom_types.py index 4737f978..1da6d421 100644 --- a/diffyscan/utils/custom_types.py +++ b/diffyscan/utils/custom_types.py @@ -2,7 +2,6 @@ class BinaryConfig(TypedDict): - hardhat_config_name: NotRequired[str] constructor_calldata: NotRequired[dict[str, str]] constructor_args: NotRequired[dict[str, list]] deployment_from: NotRequired[dict[str, str]] @@ -70,13 +69,15 @@ class GithubRepo(TypedDict): class Config(TypedDict): contracts: dict[str, str] - network: str + network: NotRequired[str] github_repo: GithubRepo dependencies: NotRequired[dict[str, GithubRepo]] explorer_hostname: str explorer_hostname_env_var: NotRequired[str] explorer_token_env_var: NotRequired[str] - explorer_chain_id: NotRequired[int] + explorer_chain_id: NotRequired[int | str] + rpc_url_env_var: NotRequired[str] + deployment_gas_limit: NotRequired[int] bytecode_comparison: NotRequired[BinaryConfig] allowed_diffs: NotRequired[AllowedDiffsConfig] fail_on_bytecode_comparison_error: NotRequired[bool] diff --git a/diffyscan/utils/explorer.py b/diffyscan/utils/explorer.py index ec13f6d4..c523bb78 100644 --- a/diffyscan/utils/explorer.py +++ b/diffyscan/utils/explorer.py @@ -743,31 +743,6 @@ def parse_compiled_contract( return contract_creation_code_without_calldata, deployed_bytecode, immutables -def get_config_value(config: dict, key: str, warn_if_missing: bool = True): - """ - Get a value from config with optional warning if missing. - - Args: - config: Configuration dictionary - key: Key to look up - warn_if_missing: Whether to warn if the key is not found - - Returns: - The config value or None if not found - """ - value = config.get(key) - if value is None and warn_if_missing: - logger.warn(f'Failed to find "{key}" in the config') - return value - - -def get_explorer_hostname(config: dict) -> str | None: - """Get explorer hostname from config.""" - value = get_config_value(config, "explorer_hostname", warn_if_missing=True) - return str(value) if value is not None else None - - def get_explorer_chain_id(config: dict) -> int | None: - """Get explorer chain ID from config.""" - value = get_config_value(config, "explorer_chain_id", warn_if_missing=False) + value = config.get("explorer_chain_id") return int(value) if value is not None else None diff --git a/diffyscan/utils/logger.py b/diffyscan/utils/logger.py index eb2c3b16..bc742f8d 100644 --- a/diffyscan/utils/logger.py +++ b/diffyscan/utils/logger.py @@ -4,16 +4,12 @@ from .constants import LOGS_PATH -CYAN = "\033[96m" -PURPLE = "\033[95m" -DARKCYAN = "\033[36m" BLUE = "\033[94m" GREEN = "\033[92m" YELLOW = "\033[93m" RED = "\033[91m" BOLD = "\033[1m" -UNDERLINE = "\033[4m" END = "\033[0m" diff --git a/tests/fixtures/full_config.json b/tests/fixtures/full_config.json index e773e9ad..66164d7e 100644 --- a/tests/fixtures/full_config.json +++ b/tests/fixtures/full_config.json @@ -29,7 +29,6 @@ }, "fail_on_bytecode_comparison_error": true, "bytecode_comparison": { - "hardhat_config_name": "mainnet_hardhat_config.js", "constructor_calldata": { "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84": "0x1234abcd" }, diff --git a/tests/fixtures/full_config.yaml b/tests/fixtures/full_config.yaml index 9c3eb163..38bd5788 100644 --- a/tests/fixtures/full_config.yaml +++ b/tests/fixtures/full_config.yaml @@ -33,7 +33,6 @@ dependencies: fail_on_bytecode_comparison_error: true bytecode_comparison: - hardhat_config_name: mainnet_hardhat_config.js constructor_calldata: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84": "0x1234abcd" constructor_args: diff --git a/tests/test_allowed_diffs.py b/tests/test_allowed_diffs.py index 7e0ed5e5..c2ca8251 100644 --- a/tests/test_allowed_diffs.py +++ b/tests/test_allowed_diffs.py @@ -25,7 +25,7 @@ def _config_with(diff_kind: str, rule: dict) -> dict: } -def test_build_effective_allowed_diffs_prefers_config_over_cli(): +def test_build_effective_allowed_diffs_uses_config(): config = { "contracts": {"0x0000000000000000000000000000000000000001": "Test"}, "allowed_diffs": { @@ -37,11 +37,7 @@ def test_build_effective_allowed_diffs_prefers_config_over_cli(): }, } - result = build_effective_allowed_diffs( - config, - cli_source_addrs=[], - cli_bytecode_addrs=["0x0000000000000000000000000000000000000001"], - ) + result = build_effective_allowed_diffs(config) assert result["bytecode"]["0x0000000000000000000000000000000000000001"] == [ { @@ -538,26 +534,6 @@ def test_build_bytecode_suggestion_entry_fallback_to_any(): assert "byte_ranges" not in suggestion -# --- build_effective_allowed_diffs: CLI-only address --- - - -def test_build_effective_allowed_diffs_cli_only_address(): - config = { - "contracts": {"0x0000000000000000000000000000000000000001": "Test"}, - } - - result = build_effective_allowed_diffs( - config, - cli_source_addrs=["0x0000000000000000000000000000000000000001"], - cli_bytecode_addrs=[], - ) - - rules = result["source"]["0x0000000000000000000000000000000000000001"] - assert len(rules) == 1 - assert rules[0]["any"] is True - assert rules[0] == {"reason": "CLI allow-source-diff", "any": True} - - # --- summarize helpers --- diff --git a/tests/test_binary_verifier.py b/tests/test_binary_verifier.py index a6b162a6..08c4f61e 100644 --- a/tests/test_binary_verifier.py +++ b/tests/test_binary_verifier.py @@ -2,34 +2,41 @@ from diffyscan.utils.binary_verifier import ( analyze_bytecode_diff, - deep_match_bytecode, parse, ) from diffyscan.utils.constants import OPCODES, PUSH0, PUSH32 -from diffyscan.utils.custom_exceptions import BinVerifierError -def test_length_mismatch_raises(): - actual = "0x6001600055fe" - expected = "0x6001600055fe6001" +def test_analyze_bytecode_diff_detects_length_mismatch(): + local = "0x6001600055fe" + remote = "0x6001600055fe6001" - with pytest.raises(BinVerifierError, match="different length"): - deep_match_bytecode(actual, expected, immutables={}) + analysis = analyze_bytecode_diff(local, remote, immutables={}) + assert analysis["exact_match"] is False + assert analysis["length_mismatch"] is True -def test_immutable_only_diff_returns_false(): - actual = "0x6001fe" - expected = "0x6002fe" - assert deep_match_bytecode(actual, expected, immutables={1: 1}) is False +def test_analyze_bytecode_diff_marks_immutable_ranges(): + local = "0x6001fe" + remote = "0x6002fe" + analysis = analyze_bytecode_diff(local, remote, immutables={1: 1}) -def test_non_immutable_diff_raises(): - actual = "0x6001fe" - expected = "0x6001fd" + assert analysis["runtime_mismatch_ranges"] == [ + {"offset": 1, "length": 1, "immutable": True} + ] - with pytest.raises(BinVerifierError, match="differences not on the immutable"): - deep_match_bytecode(actual, expected, immutables={}) + +def test_analyze_bytecode_diff_marks_non_immutable_ranges(): + local = "0x6001fe" + remote = "0x6001fd" + + analysis = analyze_bytecode_diff(local, remote, immutables={}) + + assert analysis["runtime_mismatch_ranges"] == [ + {"offset": 2, "length": 1, "immutable": False} + ] # --- Opcode parsing --- diff --git a/tests/test_diffyscan_allowlist_runtime.py b/tests/test_diffyscan_allowlist_runtime.py index 9e41e3d0..f4d33e17 100644 --- a/tests/test_diffyscan_allowlist_runtime.py +++ b/tests/test_diffyscan_allowlist_runtime.py @@ -46,13 +46,10 @@ def test_any_rule_does_not_suppress_compile_errors(monkeypatch): result = runner.process_config( "config.json", - hardhat_config_path=None, recursive_parsing=False, enable_binary_comparison=True, cache_explorer=False, cache_github=False, - cli_allowed_source_diffs=[], - cli_allowed_bytecode_diffs=[], skip_user_input=True, ) @@ -83,13 +80,10 @@ def test_any_rule_can_suppress_deployment_simulation_errors(monkeypatch): result = runner.process_config( "config.json", - hardhat_config_path=None, recursive_parsing=False, enable_binary_comparison=True, cache_explorer=False, cache_github=False, - cli_allowed_source_diffs=[], - cli_allowed_bytecode_diffs=[], skip_user_input=True, ) @@ -101,6 +95,45 @@ def test_any_rule_can_suppress_deployment_simulation_errors(monkeypatch): assert result["bytecode_stats"][0]["matched_facets"] == ["any"] +def test_process_config_normalizes_explorer_chain_id(monkeypatch): + config = { + "contracts": {ADDR: "Test"}, + "explorer_hostname": "api.etherscan.io", + "explorer_chain_id": "1", + "source_comparison": False, + } + captured = {} + _stub_process_config_dependencies(monkeypatch, config) + + def fake_get_contract_from_explorer( + token, + explorer_hostname, + contract_address, + contract_name_from_config, + chain_id=None, + use_cache: bool = False, + ): + captured["explorer_chain_id"] = chain_id + return {"name": "Test", "solcInput": {"sources": {}}} + + monkeypatch.setattr( + runner, + "get_contract_from_explorer", + fake_get_contract_from_explorer, + ) + + runner.process_config( + "config.json", + recursive_parsing=False, + enable_binary_comparison=False, + cache_explorer=False, + cache_github=False, + skip_user_input=True, + ) + + assert captured["explorer_chain_id"] == 1 + + def test_constructor_override_simulation_uses_deployment_gas_limit(monkeypatch): config = { "bytecode_comparison": {}, diff --git a/tests/test_explorer_utils.py b/tests/test_explorer_utils.py index 618a4158..a7709db0 100644 --- a/tests/test_explorer_utils.py +++ b/tests/test_explorer_utils.py @@ -1,6 +1,6 @@ import json -from diffyscan.utils.explorer import get_contract_from_explorer, get_explorer_hostname +from diffyscan.utils.explorer import get_contract_from_explorer class DummyResponse: @@ -11,11 +11,6 @@ def json(self): return self.payload -def test_get_explorer_hostname_direct(): - cfg = {"explorer_hostname": "api.etherscan.io"} - assert get_explorer_hostname(cfg) == "api.etherscan.io" - - def test_get_contract_from_explorer_uses_cache(monkeypatch, tmp_path): calls = {"count": 0}