rea-reverse

Etherscan 合约验证失败?从字节码读出编译器版本和设置

基于 REA 6.4.0更新于 约 8 分钟读完
本页目录

结论先行: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 里自己确认,工具页里有说明。

下一步

以上步骤请用于你自己部署、或经授权验证的合约。读取公开字节码是被动操作,但仍要遵守所用区块浏览器和 RPC 服务的条款;进一步运行或交互之前,请先阅读 REA 的 DISCLAIMER.md。