Etherscan 合约验证失败?从字节码读出编译器版本和设置
本页目录
结论先行:solc 会在每个合约的 runtime bytecode 末尾追加一小段 CBOR 数据,里面有精确的编译器版本(solc)和 metadata 文件的哈希(ipfs)。metadata 文件记录了你的编译设置和每个源文件的哈希。字节码最后两个字节就是这段 CBOR 的长度。先把它读出来,Etherscan 报 “bytecode does NOT match” 时就不用再瞎猜:编译器版本直接有了;再把末尾这段切掉,拿本地重新编译的结果和链上代码比一次,就能判断问题出在源码文本还是编译设置。
读这段数据可以用本站的 EVM 函数选择器提取工具,它在“编译器元数据”一栏里显示;在 Linux x64 上也可以用 REA(morluto 的开源项目)。
适合谁读:自己写的合约在 Etherscan 上验证不过的 Solidity 开发者,不管你用的是网页表单、hardhat verify 还是 forge verify-contract。
读完你能做到:
- 看懂 Etherscan 每条报错对应的是哪里不一致
- 从已部署字节码里读出 solc 版本和 metadata 哈希
- 一次对比就分清“源码不一致”还是“设置不一致”
- 知道哪些设置字节码里读不出来,只能逐个试
我实际跑了什么:下面所有数据都是 2026-10-14 用一个小代币 MyToken 编出来的(OpenZeppelin Contracts 5.6.1 的 ERC20 + Ownable,外加一个 mint 函数)。编译器用的是 npm solc 包里的 solc 0.8.37,以及从 binaries.soliditylang.org 下载的 solc 0.8.24(SHA-256 已和 list.json 核对)。一共比较了九种编译结果,全程没有连链。部署后链上保存的就是这里得到的 runtime 代码,只有 immutable 变量的值会不同,而 MyToken 没有 immutable。
Etherscan 的报错分别指向哪里
Etherscan 官方的 Common Verification Errors 列出了下面这些报错,对照关系如下:
| 报错(节选) | 通常意味着 | 去看哪里 |
|---|---|---|
| “Compiled contract deployment bytecode does NOT match the transaction deployment bytecode” | 编译器版本、优化器、其他设置或源码不同 | 下面第 2 到第 4 步 |
| “Please check if the correct bytecodehash was specified via standard-json verification.” | 你部署时的 metadata.bytecodeHash 不是默认值(比如 none) |
第 2 步:看 CBOR 里有哪些键 |
| “Please check if the correct constructor argument was entered” | 构造函数参数缺失或填错 | 不在 runtime 代码里,见第 5 步 |
| “Invalid or not supported solc version” | 选的版本字符串不在 Etherscan 的列表里 | 第 2 步给出正确版本 |
| “…File import callback not supported” | 上传了单个文件,但它 import 了别的文件 | 拍平成一个文件,或改用 standard JSON input |
Etherscan 还特别说明:metadata 哈希这项设置只能通过 standard JSON input 指定,单文件和多文件提交方式都用编译器默认值。只要你的编译配置有一点非默认,就走 standard JSON。
第 1 步:拿到 runtime bytecode
要的是 runtime(已部署)代码,不是部署交易里的 creation code。
# Foundry: "Get the runtime bytecode of a contract"
cast code 0xYourContract --rpc-url "$RPC_URL" > deployed.hex
# Plain JSON-RPC, any client
curl -s -X POST "$RPC_URL" -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0xYourContract","latest"]}' \
| jq -r .result > deployed.hex
本地重编译的对应产物是:Hardhat artifact 里的 deployedBytecode,Foundry out/<File>.sol/<Contract>.json 里的 deployedBytecode.object,或 solc standard JSON 输出里的 evm.deployedBytecode.object。我用的是最后一种:
// compile.cjs (excerpt): standard JSON in, deployedBytecode + metadata out
const solc = require('solc'); // npm solc 0.8.37
const input = { language: 'Solidity',
sources: { 'contracts/MyToken.sol': { content: src } },
settings: { optimizer: { enabled: true, runs: 200 },
outputSelection: { '*': { '*': ['evm.deployedBytecode.object', 'metadata'] } } } };
const out = JSON.parse(solc.compile(JSON.stringify(input), { import: findImports }));
第 2 步:读末尾的 CBOR
把十六进制粘贴到 EVM 工具。MyToken 这一版(开优化器,runs 200)共 2178 字节,“编译器元数据”一栏显示两个键:
| 键 | 值 |
|---|---|
ipfs |
1220e1a363b180b1bfe5871d252ad2a7fbe061ddbafec5c3afc717540f3bbb884254 |
solc |
0.8.37 (000825) |
solc 的正式版用 3 个字节记录版本号:主版本、次版本、补丁号。00 08 25 就是 0.8.37,页面会把解码后的版本和原始字节并排显示(见 src/tools/evm/ui/main.ts 的 metaValue)。Solidity 官方 metadata 文档还提到:预发布版写的是完整版本字符串;老版本编译器用 bzzr0/bzzr1(Swarm)哈希;有的构建还会多一个 experimental 键。本文只实测了 ipfs 和 solc 两种。
不想用网页也行,这段数据短到可以手工读:
python3 - <<'EOF'
b = bytes.fromhex(open("deployed.hex").read().strip().removeprefix("0x"))
n = int.from_bytes(b[-2:], "big") # CBOR length in the last 2 bytes
print(len(b), n, b[-(n + 2):].hex())
EOF
2178 51 a2646970667358221220e1a363b180b1bfe5871d252ad2a7fbe061ddbafec5c3afc717540f3bbb88425464736f6c63430008250033
逐段看:a2 表示有两项的 CBOR map;64 69706673 是文本 “ipfs”;58 22 表示后面跟 34 字节(1220 加一个 SHA-256);64 736f6c63 是 “solc”;43 表示后面跟 3 字节;最后的 0033 是长度 51。
哈希交叉验证:ipfs 的值是这次编译生成的 metadata JSON 的 IPFS CIDv0 多重哈希。我把 MyToken 的 metadata 文件按单块 UnixFS 节点算了一遍,结果正好是 1220e1a3…884254,也就是 QmdXSwhmsyHqEoGw4FiAy5vvgZwRokeGZDVQP2rQpSXe6X。所以这个哈希就是 metadata 文件的指纹,而 metadata 里装着你的编译设置和每个源文件的 keccak256。
从 CBOR 看 bytecodeHash 设置
“bytecodehash” 那条报错,看键就能回答。默认的 bytecodeHash: "ipfs" 下,末尾是 51 字节,含 ipfs 和 solc。我改用 "metadata": {"bytecodeHash": "none"} 重新编译后,末尾缩成 10 字节(a164736f6c6343000825000a),只剩 solc。所以:链上代码里没有 ipfs 键,就用 standard JSON 并把 bytecodeHash 设为 none 去验证;连 CBOR 都没有,多半是编译时用了 appendCBOR: false。
第 3 步:切掉 CBOR 再比较
把链上代码和本地重编译结果末尾的 CBOR 连同两个长度字节一起去掉,再比较剩下的部分。这一次比较就能把问题一分为二。下表是我的结果,“主体”指去掉末尾后的代码;基准是 solc 0.8.37、开优化器、runs 200、默认 EVM 版本(osaka):
| 相对基准的改动 | 大小 | 主体相同? | ipfs 变了? |
solc |
|---|---|---|---|---|
| 无(重编译一次) | 2178 | 是 | 否 | 0.8.37 |
加一行 // 注释 |
2178 | 是 | 是 | 0.8.37 |
加一行 /// NatSpec 注释 |
2178 | 是 | 是 | 0.8.37 |
| 同一文件换个路径 | 2178 | 是 | 是 | 0.8.37 |
bytecodeHash: none |
2137 | 是 | 键消失 | 0.8.37 |
| 优化器 runs 改为 1000 | 2392 | 否 | 是 | 0.8.37 |
| 关闭优化器 | 4380 | 否 | 是 | 0.8.37 |
viaIR: true |
2044 | 否 | 是 | 0.8.37 |
evmVersion: paris |
2246 | 否 | 是 | 0.8.37 |
| solc 0.8.24 | 2200 | 否 | 是 | 0.8.24(000818) |
怎么读这张表:
- 主体相同,只有
ipfs不同:可执行代码没问题,差的是 metadata,原因可能是源码文本(哪怕只是一行注释或一个空格)、文件路径或文件名、import 重映射,或其他只影响 metadata 的设置。Etherscan 的精确匹配要求 metadata 也一致。找回部署时的源码目录和路径,最好直接用构建工具留下的 build-info 或 artifact,用 standard JSON input 提交。 - 主体不同:某个影响代码生成的设置不对。先把编译器版本对上,这个刚才已经从末尾读出来了;然后依次检查优化器开关和 runs、
viaIR、EVM 版本。这些设置写在 metadata JSON 里,但不在 CBOR 本身,CBOR 只存哈希。所以字节码不会告诉你 “runs = 200”,只能一个个候选值试,直到主体对上。
表里还能看出一点:solc 0.8.37 的默认 EVM 版本是 osaka,0.8.24 是 shanghai(都取自各自生成的 metadata 里的 settings.evmVersion)。如果当初用旧工具链部署、现在用新工具链验证,默认值可能已经悄悄变了。
第 4 步:让验证工具用同样的设置
确定了具体值,就显式传进去,不要依赖默认值。
Foundry:forge verify-contract 文档列出了 --compiler-version、--num-of-optimizations(别名 --optimizer-runs)、--via-ir、--evm-version、--constructor-args 和 --guess-constructor-args。另外还有 --show-standard-json-input,会打印出它将要提交的完整输入,拿来和部署时的配置对比很方便。
forge verify-contract 0xYourContract src/MyToken.sol:MyToken \
--chain sepolia --compiler-version v0.8.37+commit.f401782d \
--num-of-optimizations 200 --show-standard-json-input > submitted.json
commit 后缀来自 solc 的完整版本字符串,我这次编译报告的是 0.8.37+commit.f401782d。
Hardhat:当前的验证指南用法是 hardhat verify --network sepolia <address> <constructor args>。文档提醒:verify 默认用 production 构建配置,换一个配置编出来的字节码就不一样。如果部署时用的是别的配置,加 --build-profile 对齐。
本文没有对真实浏览器跑这两个工具,上面的参数都来自两个项目在 2026-10-14 的官方文档。
第 5 步:构造参数,以及是不是部署错了合约
构造参数不在 runtime 代码里。 MyToken 的构造函数接收一个 owner 地址,这个值附在部署交易的 creation code 后面,eth_getCode 返回的内容里根本没有它。Etherscan 文档说可以从原始部署交易里取,或者从 creation bytecode 末尾取;Foundry 的 --guess-constructor-args 可以帮你自动尝试。
确认是你以为的那个合约。 如果主体不同、换什么设置都对不上,就看接口。EVM 工具会列出它从字节码恢复出的选择器,这一版找到 13 个,除了 ERC-20 那一组,还有 mint(address,uint256) 和 owner()。拿它们和你编译输出里的 evm.methodIdentifiers 对一下:少一个或多一个选择器,说明你验证的是另一个合约或另一个版本,跟设置无关。怎么用 solc 自己的输出核对选择器,见选择器恢复指南。
用 REA 6.4.0 读同一段数据
REA 6.4.0 的 rea inspect-evm-interface <file> hex --json(MCP 工具名 inspect_evm_interface)用的是和浏览器工具相同的 EVMole 0.9.3。6.4.0 的这个 provider 只接受 Linux x64。我在 Apple Silicon Mac 上运行,CLI 按文档所说直接拒绝:
{"code":"capability_unavailable","category":"unsupported_provider",
"message":"Initial real-verified offline EVM interface profile supports Linux x64 only."}
为了看 6.4.0 实际会报告什么,我直接运行了它自带的 worker 模块(dist/evm/EvmoleInterfaceWorker.js,worker 本身不检查平台),输入同一个文件。它返回 13 个函数,以及这段元数据:
{"bytecodeOffset":2125,"cborLength":51,"entries":[
{"key":"ipfs","value":{"type":"bytes","value":"1220e1a363b180b1bfe5871d252ad2a7fbe061ddbafec5c3afc717540f3bbb884254"}},
{"key":"solc","value":{"type":"bytes","value":"000825"}}]}
bytecodeOffset 2125 就是 CBOR 的起点:2178 − 51 − 2。在受支持的 Linux x64 主机上,这段数据出现在 Evidence 记录的 raw_result.metadata 里。provider 的 limitations 写明这些值是“原样保留、不保证精度”的,所以把 REA 的这份当作存档,不要当成第二个独立判断。REA 不会替你把 000825 解码成 “0.8.37”,也不会访问任何链。
常见问题
能从字节码里读出优化器 runs 吗?
不能直接读。runs、viaIR 和 EVM 版本都在 metadata JSON 里,字节码只带这个文件的哈希。如果你还留着构建时的 metadata 文件,设置就在里面,而且可以按上面的方法算一下哈希,确认就是这份文件。没有的话,只能用候选设置反复重编译,直到主体对上。
为什么加一行注释就验证不过?
metadata 里记录了每个源文件的 keccak256,源码文本任何改动都会让 metadata 哈希变化。我的测试里,只加一行 // 注释,可执行主体逐字节不变,只有 ipfs 的值变了,而 Etherscan 的精确匹配连这部分也要比。
链上代码末尾没有 CBOR,是出问题了吗?
不一定。可能编译时用了 appendCBOR: false,也可能代码根本不是 solc 编出来的。这时 EVM 工具不会显示元数据一栏。用能还原这些设置的 standard JSON input 去验证即可。
浏览器工具会上传我的字节码吗?
不会。分析在你本机的 Web Worker 里完成,怎么在 DevTools 里自己确认,工具页里有说明。
下一步
- 用 REA 离线从字节码恢复 EVM 函数选择器:把每个选择器、参数和可变性都和 solc 的输出对一遍。
- Etherscan 上的未验证合约:从字节码判断是不是代理合约、是不是代币:如果你的合约放在代理后面,实现合约也要验证。
- 安装 REA 并接入 MCP 客户端。
以上步骤请用于你自己部署、或经授权验证的合约。读取公开字节码是被动操作,但仍要遵守所用区块浏览器和 RPC 服务的条款;进一步运行或交互之前,请先阅读 REA 的 DISCLAIMER.md。