Overview
On 2026-11-03, Scroll received a bug bounty report about the bundle proof verifier deployed on Ethereum mainnet. On 2026-11-10, we registered a fixed verifier for batch version 10. We verified that the bug was not exploited on Scroll mainnet.
Summary of the Bug
ZkEvmVerifierPostFeynman (0x7966FE0De13e3B81533470640Ac489223476B254) verifies OpenVM bundle proofs for ScrollChain. It built the verifier input by hand and sent it to the OpenVM 2.0 Halo2 verifier with a raw staticcall. The first 4 bytes of that call came from the caller-supplied proof.
The OpenVM 2.0 verifier exposes a typed function verify(bytes,bytes,bytes32,bytes32) (selector 0x24270d54) next to its Halo2 fallback. A proof that starts with 0x24270d54 reaches that function instead of the fallback. The function then reads the public values from offsets the caller controls. An old, valid proof could therefore be accepted for a new public input, which would allow finalizing an arbitrary state root and withdraw root.
Exploiting the bug required either the permissioned prover role or enforced batch mode, which opens after 7 days without finalization. Neither was used to exploit it: enforced mode never opened, and every finalization used a regular proof. Earlier verifier versions were not affected: their Halo2 verifiers have no function selector, so every call reached the fallback.
The Fix
The new verifier calls the OpenVM wrapper through its typed entry point, so the selector, commitments and public-input hash all come from the contract, never from the proof. The input that reaches the Halo2 fallback is byte-identical to the previous version. The constructor now also rejects an empty verifier address and out-of-range digests.
- New verifier: 0xb15417122fB6E853d84e498517268d33d7301e74
- Registration in
MultipleVersionRollupVerifierin this transaction. - Code change: scroll-tech/scroll-contracts#181 (merge commit
8264260)
What We Verified
- Equivalence: a differential fuzz test and a mainnet-fork byte comparison show the new verifier passes identical input to the Halo2 verifier.
- Compatibility: 233 past finalize transactions, replayed through
ScrollChainon mainnet forks, finalize with the same roots; tampered copies are rejected. - Upgrade path: every Safe and timelock transaction was simulated before signing.
- Bytecode: the deployed verifier matches #181 and keeps the previous verifier’s settings.
- No exploitation: all 748 finalizations while the vulnerable verifier was active used regular proofs with no malicious selector prefix, confirmed by both an RPC node and Etherscan. Enforced mode never opened.
Verify It Yourself
Copy this prompt into your coding agent:
Verify Scroll's verifier upgrade on Ethereum mainnet. Use read-only RPC calls and public sources only; do not send transactions. Report PASS or FAIL for each check, with evidence.
Old verifier: 0x7966FE0De13e3B81533470640Ac489223476B254,
New verifier: 0xb15417122fb6e853d84e498517268d33d7301e74,
MultipleVersionRollupVerifier: 0x4CEA3E866e7c57fD75CB0CA3E9F5f1151D4Ead3F,
ScrollChain: 0xa13BAF47339d63B743e7Da8741db5456DAc1E556,
1. Compatibility: read the verified Etherscan sources of the old verifier, the new verifier and its `plonkVerifier` (0x0d2A59fd7060460F07c30C2817b4E79576da01C6, `OpenVmHalo2Verifier`). Confirm that the payload the old verifier built by hand is byte-for-byte the payload `_constructProof` builds from the new verifier's arguments: same proof bytes, same digests, and the same 32 public-input-hash bytes in the same order.
2. Bytecode: build `ZkEvmVerifierPostFeynman` from scroll-tech/scroll-contracts at commit 8264260718fc7bfcde84bb77ba2b90352d02df76 with `forge build`. The new verifier's deployed runtime equals the build output at every byte outside its immutable references.
3. Immutables: the old and new verifiers return the same `plonkVerifier()`, `verifierDigest1()`, `verifierDigest2()` and `protocolVersion()`.
4. Registration: `latestVerifier(10)` on MultipleVersionRollupVerifier returns the new verifier.
5. Live finalization: the `finalizeBundlePostEuclidV2` transactions sent to ScrollChain after the registration succeeded, and their traces call the new verifier.