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.
Reproduce
Bytecode comparison crashes with
KeyError: 'contracts'whenever solc fails to compile the explorer-suppliedsolcInput. The actual solc diagnostic is lost.Minimal reproducer (Mainnet USDe
0x4c9EDD5852cd905f086C759E8383e09bff1E68B3— the deployed Ethena USDe token; production source on Ethena's public BBP repo):Source-diff completes; bytecode step then crashes:
Root cause
compile_contracts()(utils/compiler.py:94) returnsjson.loads(solc.stdout)verbatim. solc's standard-json output is{"contracts": {...}, "sources": {...}}on success, or{"errors": [...]}when compilation fails outright (nocontractskey).Caller at
explorer.py:703indexesresult["contracts"]without checking forresult.get("errors"), so the actual diagnostic gets thrown away and replaced with a context-freeKeyError.Suggested fix
In
compile_contracts()or at the call site: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_contractsto 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
solcInputreferences foundry-stylelib/openzeppelin-contracts/...paths, but the import inUSDe.soluses@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.