Skip to content

fix(foundry): VerifyAll supports linked libs; enable optimizer#414

Open
rin-st wants to merge 2 commits intomainfrom
fix-verify-all
Open

fix(foundry): VerifyAll supports linked libs; enable optimizer#414
rin-st wants to merge 2 commits intomainfrom
fix-verify-all

Conversation

@rin-st
Copy link
Copy Markdown
Member

@rin-st rin-st commented Apr 21, 2026

Follow-up to #411

Summary

  • VerifyAll.s.sol silently underflowed on any contract that links an external library, aborting yarn verify with Panic(0x11).
  • foundry.toml ships without optimizer settings, so large generated contracts (e.g., ZK verifiers) can't deploy — they're rejected by EIP-170's 24,576-byte runtime limit.
  • Both surface together on the SpeedRunEthereum ZK-voting challenge; repro steps below.

Problem

1. VerifyAll.s.sol breaks on linked contracts

For contracts that use external libraries, solc leaves __$<17-byte-hash>$__ placeholders in bytecode.object. The current script does:

bytes memory compiledBytecode =
    abi.decode(vm.parseJson(artifact, ".bytecode.object"), (bytes));
bytes memory constructorArgs = BytesLib.slice(
    deployedBytecode, compiledBytecode.length,
    deployedBytecode.length - compiledBytecode.length
);

Two bugs chained:

  1. parseJson/abi.decode(..., (bytes)) cannot hex-decode a string containing _/$, so it silently falls back to the JSON string-encoding path. The returned length is the character
    count (~2× the real byte length).
  2. Even length-corrected, the script never passes --libraries <path>:<name>:<addr> to forge verify-contract, so Etherscan has no way to link.

Result: deployedBytecode.length - compiledBytecode.length underflows → verification aborts for every linked contract (Semaphore, LeanIMT, PoseidonT3, OZ libs compiled standalone,
etc.).

  1. No optimizer → EIP-170 rejections

Stock packages/foundry/foundry.toml has no optimizer profile. Unoptimized bytecode from Barretenberg's Noir→Solidity verifier is ~30 KB runtime, over the 24,576-byte limit. yarn deploy --network sepolia fails before we ever reach yarn verify.

Reproduction

Anyone can reproduce both issues end-to-end with the SpeedRunEthereum ZK-voting challenge:

npx create-eth@latest -e https://github.com/scaffold-eth/se-2-challenges/tree/challenge-zk-voting-foundry -s foundry my-zk-voting
cd my-zk-voting

Then fill in the contracts one of three ways:

  • Hand-solve all 10 checkpoints.
  • Use the AI-guided mode — in Claude Code inside the project, run /start, then /skip repeatedly until all 10 checkpoints are complete.
  • Or just drop in the reference solution directly (e.g., check out the solution branch of the challenge repo, or copy the finished contracts from there).

After the contracts are written:

yarn deploy --network sepolia # fails: HonkVerifier > 24,576 bytes (bug 2)

With optimizer enabled, deploy succeeds, then:

yarn verify --network sepolia # Voting panics with arithmetic underflow (bug 1)

Fix

Two changes, same file ownership (Foundry package), no new deps.

  1. packages/foundry/script/VerifyAll.s.sol
  • Length: read bytecode.object as a string (via vm.parseJsonString, with a generic parseJson + string-decode fallback) and compute byte length as (chars - "0x") / 2. Placeholder
    and resolved address are both 20 bytes, so length is placeholder-agnostic.
  • Library discovery (pure Solidity): enumerate linkReferences outer keys via vm.parseJsonKeys. For each library source path, iterate broadcast transactions; for each contractName,
    recompute solc's placeholder — "$" + hex(keccak256(path ":" name)[0:17]) + "$" — and check whether it appears in the compiled bytecode. A hit identifies which deployment
    satisfies the link.
  • Dynamic args: append --libraries <path>:<name>:<address> pairs to the existing forge verify-contract invocation, one pair per discovered library.
  • Artifact-path fallback: find-based lookup for cases where the source filename differs from the contract name (e.g., Verifier.sol exports HonkVerifier).

No Python/jq/external tools; everything is in-script.

  1. packages/foundry/foundry.toml
  optimizer = true
  optimizer_runs = 200

Reasonable defaults for any foundry-based project and unblocks large generated contracts. (Also tried via_ir = true for further savings — it blew up on Yul stack-too-deep inside
the Barretenberg-generated assembly, so leaving it off.)

Test plan

  • Fresh zk-voting challenge, full flow: yarn deploy + yarn verify on Sepolia, both succeed
  • Contract with no libraries → verification unchanged (empty lib list, no --libraries flags added)
  • Contract linking one external library → verification succeeds with one --libraries flag
  • Contract linking two+ libraries → all links resolved
  • Source filename ≠ contract name (e.g., Foo.sol exports Bar) → artifact located via fallback
  • Regression: existing un-linked, already-working deploys verify as before
  • Existing packages/foundry/test/* suite passes with optimizer enabled

@rin-st rin-st requested a review from technophile-04 April 21, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant