Etherscan Bytecode Does Not Match? Fix a Failed Hardhat or Forge Verify
On this page
When Etherscan says your bytecode does not match, the deployed code itself tells you most of the answer. solc appends a small CBOR map to the end of the runtime bytecode with the exact compiler version and a hash of the metadata file (your settings plus the hash of every source file). Read it, select that compiler, then compare your rebuild with the deployed code with the tail cut off: if the rest is identical, the problem is your source text or paths; if it differs, it is a compiler setting.
You can read the tail in your browser with this site’s EVM function selector extractor (it runs locally in a Web Worker; nothing is uploaded), under Compiler metadata, or by hand with the script in Step 2. This works the same whether verification failed in the web form, hardhat verify or forge verify-contract.
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. 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. If you still have the metadata JSON from the original build (Hardhat build-info, Foundryout/), the settings are written in it, and hashing it as shown in Step 2 confirms it is the right file.
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. Unverified contract: proxy or token? shows how to check inferred selectors against solc’s own output, and if your contract sits behind a proxy, verify the implementation too.