Skip to content

KeyError: 'contracts' swallows solc compilation errors during bytecode verification #157

@tamtamchik

Description

@tamtamchik

Reproduce

Bytecode comparison crashes with KeyError: 'contracts' whenever solc fails to compile the explorer-supplied solcInput. The actual solc diagnostic is lost.

Minimal reproducer (Mainnet USDe 0x4c9EDD5852cd905f086C759E8383e09bff1E68B3 — the deployed Ethena USDe token; production source on Ethena's public BBP repo):

cat > /tmp/repro.yaml <<'YAML'
contracts:
  "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3": USDe
network: mainnet
explorer_hostname: api.etherscan.io
explorer_chain_id: 1
explorer_token_env_var: ETHERSCAN_API_KEY
github_repo:
  url: https://github.com/ethena-labs/bbp-public-assets
  commit: f3e56d5f06bfef82367d5d5b561398e91d5bebc1
  relative_root: contracts
dependencies: {}
YAML

ETHERSCAN_API_KEY=<key> GITHUB_API_TOKEN=$(gh auth token) REMOTE_RPC_URL=https://eth.drpc.org \
  uv run diffyscan /tmp/repro.yaml

Source-diff completes; bytecode step then crashes:

File "diffyscan/utils/explorer.py", line 703, in compile_contract_from_explorer
    compiled_contracts = compile_contracts(compiler_path, input_settings)["contracts"].values()
KeyError: 'contracts'

Root cause

compile_contracts() (utils/compiler.py:94) returns json.loads(solc.stdout) verbatim. solc's standard-json output is {"contracts": {...}, "sources": {...}} on success, or {"errors": [...]} when compilation fails outright (no contracts key).

Caller at explorer.py:703 indexes result["contracts"] without checking for result.get("errors"), so the actual diagnostic gets thrown away and replaced with a context-free KeyError.

Suggested fix

In compile_contracts() or at the call site:

result = compile_contracts(compiler_path, input_settings)
if "contracts" not in result:
    raise CompileError(f"solc reported errors: {result.get('errors', result)}")

This surfaces the real failure (missing remapping, EVM-version mismatch, source incompatibility, etc.) instead of the synthetic KeyError.

What the swallowed error actually says

In-memory monkey-patch of compile_contracts to print the dropped output reveals (for the reproducer above):

{
  "errors": [
    {
      "errorCode": "6275",
      "formattedMessage": "ParserError: Source \"lib/openzeppelin-contracts/contracts/token/ERC20/extensions/draft-ERC20Permit.sol\" not found: File not found. Searched the following locations: \"\".",
      "severity": "error",
      "sourceLocation": { "file": "contracts/USDe.sol" },
      "type": "ParserError"
    }
  ]
}

Etherscan's stored solcInput references foundry-style lib/openzeppelin-contracts/... paths, but the import in USDe.sol uses @openzeppelin/contracts/... — and remappings aren't being honored. That's a separate symptom worth its own issue once the KeyError stops swallowing it.

Impact

Bytecode-vs-recompiled-source verification (the second half of diffyscan's value) is silently unusable for any contract whose explorer-supplied solcInput fails to compile, with no signal as to why. Users currently work around with --skip-binary-comparison, losing the bytecode integrity check.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions