Etherscan Verification Failed? Read solc Version from Bytecode
On this page
Short answer: solc appends a small CBOR map to the end of every contract’s runtime bytecode. It holds the exact compiler version (solc) and a hash of the metadata file (ipfs). The metadata file records your settings and the hash of every source file. The last two bytes of the code give the length of that map. Read it first, and an Etherscan “bytecode does NOT match” error stops being a guessing game. You know which compiler to select. Then you compare your rebuild with the deployed code with that tail cut off, and the result tells you whether the problem is your source text or your compiler settings.
You can read the tail in your browser with this site’s EVM function selector extractor, which shows it under Compiler metadata. You can also read it with REA, the open-source project by morluto, on Linux x64.
Who this is for: Solidity developers whose own contract won’t verify on Etherscan, through the web form, hardhat verify or forge verify-contract.
What you will be able to do:
- Map each Etherscan error message to what actually differs
- Read the solc version and metadata hash from deployed bytecode
- Tell a source-text mismatch from a settings mismatch in one comparison
- Know which settings the bytecode can’t tell you about, and test those
What I ran: everything below was built on 2026-10-14 from a small token, MyToken (OpenZeppelin Contracts 5.6.1 ERC20 + Ownable, plus a mint function). I compiled it with solc 0.8.37 from the solc npm package and with solc 0.8.24 downloaded from binaries.soliditylang.org (SHA-256 checked against list.json). I compared nine builds. No chain was involved. The bytecode a deployment stores is the same runtime code these builds produce, apart from immutable values, and MyToken has none.
The Etherscan messages, and what they point to
Etherscan’s Common Verification Errors page lists these messages. Here is where each one sends you:
| Message (abridged) | What it usually means | Where to look |
|---|---|---|
| “Compiled contract deployment bytecode does NOT match the transaction deployment bytecode” | Version, optimizer, settings or source differ | Steps 2–4 below |
| “Please check if the correct bytecodehash was specified via standard-json verification.” | Your build used a different metadata.bytecodeHash (for example none) |
Step 2: the CBOR keys |
| “Please check if the correct constructor argument was entered” | Constructor arguments are missing or wrong | Not in the runtime code; see Step 5 |
| “Invalid or not supported solc version” | The version string you picked isn’t on Etherscan’s list | Step 2 gives you the right one |
| “…File import callback not supported” | You uploaded one file that imports others | Flatten it, or use standard JSON input |
Etherscan notes that the metadata hash setting can only be changed through standard JSON input. Single-file and multi-file submissions use the compiler defaults. If you built with anything unusual, standard JSON is the safe route.
Step 1: Get the runtime bytecode
You need the runtime (deployed) code, not the creation code that went into the deployment transaction.
# 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
For your local rebuild, the same bytes are deployedBytecode in a Hardhat artifact, deployedBytecode.object in Foundry’s out/<File>.sol/<Contract>.json, or evm.deployedBytecode.object in solc’s standard JSON output. I used the last one:
// 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 }));
Step 2: Read the CBOR tail
Paste the hex into the EVM tool. For the 2,178-byte MyToken build (optimizer on, 200 runs), the Compiler metadata section showed two keys:
| Key | Value |
|---|---|
ipfs |
1220e1a363b180b1bfe5871d252ad2a7fbe061ddbafec5c3afc717540f3bbb884254 |
solc |
0.8.37 (000825) |
Release builds of solc store their version as three bytes: major, minor, patch. 00 08 25 is 0.8.37. The page shows the decoded value next to the raw bytes (metaValue in src/tools/evm/ui/main.ts). The Solidity metadata docs add that pre-release builds write a full version string instead. Older compilers used bzzr0/bzzr1 (Swarm) hashes, and some builds add an experimental key. I only tested ipfs and solc here.
You don’t need the page to check this. The raw tail is short enough to read by hand:
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 is a CBOR map with two entries. 64 69706673 is the text “ipfs”, 58 22 introduces 34 bytes (1220 plus a SHA-256), 64 736f6c63 is “solc”, 43 introduces 3 bytes, and 0033 is 51, the length.
Cross-check of the hash. The ipfs value is the IPFS CIDv0 multihash of the metadata JSON that solc produced for this build. I hashed MyToken’s metadata file as a single-chunk UnixFS node and got exactly 1220e1a3…884254, which is QmdXSwhmsyHqEoGw4FiAy5vvgZwRokeGZDVQP2rQpSXe6X. So that hash is a fingerprint of the metadata file, and the file contains your settings and the keccak256 of every source file.
What the CBOR tells you about bytecodeHash
The keys themselves answer the “bytecodehash” error. With the default bytecodeHash: "ipfs", the tail was 51 bytes with ipfs and solc. When I rebuilt with "metadata": {"bytecodeHash": "none"}, the tail shrank to 10 bytes (a164736f6c6343000825000a) with only solc. If your deployed code shows no ipfs key, verify through standard JSON with bytecodeHash set to none. If it has no CBOR tail at all, the build probably used appendCBOR: false.
Step 3: Strip the tail and compare
Cut the CBOR tail and its two length bytes from both the deployed code and your rebuild, then compare what is left. This one comparison splits the problem in two. Here is what I got. “Body” means the code without the tail, and the baseline is solc 0.8.37, optimizer on, 200 runs, default EVM version (osaka):
| Change from baseline | Size | Body identical? | ipfs changed? |
solc |
|---|---|---|---|---|
| None (rebuild) | 2178 | yes | no | 0.8.37 |
A // comment added |
2178 | yes | yes | 0.8.37 |
A /// NatSpec comment added |
2178 | yes | yes | 0.8.37 |
| Same file at another path | 2178 | yes | yes | 0.8.37 |
bytecodeHash: none |
2137 | yes | key gone | 0.8.37 |
| Optimizer runs 1000 | 2392 | no | yes | 0.8.37 |
| Optimizer off | 4380 | no | yes | 0.8.37 |
viaIR: true |
2044 | no | yes | 0.8.37 |
evmVersion: paris |
2246 | no | yes | 0.8.37 |
| solc 0.8.24 | 2200 | no | yes | 0.8.24 (000818) |
Read it this way:
- Body identical, only
ipfsdiffers: your executable code is right. The metadata differs: source text (even a comment or whitespace), file path or name, import remappings, or another setting that changes only the metadata. Etherscan’s exact match needs the metadata too. Recover the exact source tree and paths you deployed from, ideally from the build-info or artifact your tool kept, and submit standard JSON input. - Body differs: a setting that changes code generation is wrong. Fix the compiler version first, since you just read it from the tail. Then check the optimizer flag and runs,
viaIR, and the EVM version, in that order. These settings are inside the metadata JSON, but not in the CBOR tail itself, which only carries the hash. So the bytecode can’t tell you “runs = 200”. You test candidates until the body matches.
One more result from the table: in 0.8.37 the default EVM version was osaka, and in 0.8.24 it was shanghai (both read from the settings.evmVersion field of the metadata each compiler produced). If you deployed with an older toolchain and now verify with a newer one, the default may have moved under you.
Step 4: Make your verify tool use the same settings
Once you know the values, pass them explicitly instead of relying on defaults.
Foundry. The forge verify-contract reference lists --compiler-version, --num-of-optimizations (alias --optimizer-runs), --via-ir, --evm-version, --constructor-args and --guess-constructor-args. It also lists --show-standard-json-input, which prints the exact input it will submit. That makes it easy to compare against what you deployed.
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
The commit suffix comes from solc’s long version string. My build reported 0.8.37+commit.f401782d.
Hardhat. The current verification guide uses hardhat verify --network sepolia <address> <constructor args>. It warns that verify builds with the production profile by default, and that a different profile produces different bytecode. If your deployment used another profile, pass --build-profile to match it.
I didn’t run either tool against a live explorer for this guide. The flags above come from each project’s documentation as published on 2026-10-14.
Step 5: Constructor arguments and the wrong contract
Constructor arguments are not in the runtime code. MyToken’s constructor takes an owner address. That value is appended to the creation code in the deployment transaction, and it never appears in what eth_getCode returns. Etherscan’s docs say you can take it from the original deployment, or from the end of the creation bytecode. Foundry can try that for you with --guess-constructor-args.
Make sure it’s the contract you think it is. If the body differs and no setting fixes it, check the interface. The EVM tool lists the selectors it recovers from the bytecode. For this build it found 13, including mint(address,uint256) and owner() next to the ERC-20 set. Compare them with evm.methodIdentifiers from your build. A missing or extra selector means you are verifying a different contract or version, not facing a settings problem. The selector recovery guide explains how to check selectors against solc’s own output.
Reading the same tail with REA 6.4.0
REA 6.4.0 runs rea inspect-evm-interface <file> hex --json (MCP tool inspect_evm_interface) with the same EVMole 0.9.3 the browser tool uses. The 6.4.0 provider accepts only Linux x64. On my Apple Silicon Mac the CLI stopped right away, as documented:
{"code":"capability_unavailable","category":"unsupported_provider",
"message":"Initial real-verified offline EVM interface profile supports Linux x64 only."}
To see what 6.4.0 reports, I ran its bundled worker module (dist/evm/EvmoleInterfaceWorker.js) directly on the same file. The worker itself has no host check. It returned 13 functions and this metadata block:
{"bytecodeOffset":2125,"cborLength":51,"entries":[
{"key":"ipfs","value":{"type":"bytes","value":"1220e1a363b180b1bfe5871d252ad2a7fbe061ddbafec5c3afc717540f3bbb884254"}},
{"key":"solc","value":{"type":"bytes","value":"000825"}}]}
bytecodeOffset 2125 is where the CBOR map starts: 2178 − 51 − 2. On a supported Linux x64 host, this block appears under raw_result.metadata in the Evidence record. One of the provider’s limitations says it keeps these values “without precision claims”, so treat REA’s copy as a record, not a second opinion. REA doesn’t decode 000825 into “0.8.37” for you, and it doesn’t fetch anything from a chain.
FAQ
Can I read the optimizer runs from the bytecode?
Not directly. Runs, viaIR and the EVM version live in the metadata JSON, and the bytecode carries only that file’s hash. If you still have the metadata file from your build, the settings are right there, and you can confirm it’s the right file by hashing it as shown above. Without it, you rebuild with candidate settings until the body matches.
Why does adding a comment break verification?
The metadata includes the keccak256 of each source file, so any text change gives a new metadata hash. In my test, a single // comment left the executable body byte-identical and changed only the ipfs value. Etherscan’s exact match compares that too.
My deployed code has no CBOR tail. Is something wrong?
Not necessarily. The build may have used appendCBOR: false, or the code may not come from solc at all. The EVM tool then shows no metadata section. Verify with standard JSON input that reproduces those settings.
Does the browser tool upload my bytecode?
No. The analysis runs in a Web Worker on your device. The tool page describes how to confirm that in DevTools.
Next steps
- Recover EVM function selectors from bytecode offline with REA: check every selector, argument and mutability against solc.
- Unverified contract on Etherscan? Check if it’s a proxy or a token: if your verified contract sits behind a proxy, verify the implementation too.
- Install REA and connect it to your MCP client.
Use these steps on contracts you deployed or are authorized to verify. Reading public bytecode is passive, but follow the terms of the explorer and RPC provider you use, and see REA’s DISCLAIMER.md before you move on to anything that runs or interacts with code.