rea-reverse

Etherscan 合约验证失败、字节码不匹配?找出不一致的源码或编译设置

发布者 rea-reverse更新于 约 6 分钟读完
本页目录

Etherscan 提示字节码不匹配时,答案大半就写在链上代码里。solc 会在 runtime bytecode 末尾追加一小段 CBOR,里面有精确的编译器版本和 metadata 文件的哈希(metadata 记录了编译设置和每个源文件的哈希)。先读出版本、选对编译器,再把这段末尾切掉,拿本地重编译结果和链上代码比一次:剩下的部分完全一样,问题在源码文本或路径;不一样,就是编译设置不对。

读这段数据可以用本站的 EVM 函数选择器提取工具(在本机 Web Worker 里运行,不上传),看“编译器元数据”一栏;也可以用第 2 步的脚本手工读。不管是网页表单、hardhat verify 还是 forge verify-contract 验证失败,方法都一样。

我实际跑了什么:下面所有数据都是 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,页面会把解码后的版本和原始字节并排显示。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”,只能一个个候选值试,直到主体对上。如果还留着当初构建的 metadata JSON(Hardhat 的 build-info、Foundry 的 out/),设置就写在里面,按第 2 步的方法算一下哈希就能确认是不是同一份。

表里还能看出一点: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 自己的输出核对选择器,见从字节码判断代理合约和代币;如果你的合约放在代理后面,实现合约也要验证。