Most ZK bridge writeups spend their energy on the circuit.
Proving systems, constraint counts, trusted setup ceremonies, light-client sync committees. That work matters. It is also not where production bridges usually lose money once the math is competent.
Onchain, the user-facing claim is simple: a proof verified, therefore mint, unlock, or credit. The contract that turns verify == true into value movement is the verifier wrapper. That wrapper decides which public inputs count, which messages can be replayed, who can rotate the verifying key, and what the receiving handler is allowed to do with a valid proof.
If your review stops at "the SNARK verifies," you audited the proof system. You did not audit the bridge.
This note is for teams shipping ZK bridges, light-client relays, or zk-messaging, and for reviewers who still treat the proving system and the settlement path as the same object.
Circuit soundness is not bridge correctness
A SNARK or STARK can be internally sound and still sit under a broken product.
Circuit review asks: does the witness satisfy the constraints under this proving system?
ZK bridge security asks: when the destination chain accepts a proof, does the resulting state transition match the economic event the protocol intended, once, with the right assets and recipients?
Those are different scopes. Conflating them is how teams ship a "ZK secured" bridge that still fails like a poorly bound messaging adapter.
Guardian and multisig bridges fail when attestation policy is wrong. ZK bridges fail when the onchain interpreter of a valid proof is wrong. Different cryptography. Same settlement problem.
What the verifier contract actually does
A typical destination path looks like this:
- Relayer submits proof bytes plus a public input vector
- Onchain verifier checks the proof against a verifying key
- Wrapper binds those public inputs to a message, nullifier, or state root
- Settlement mints, unlocks, or calls a handler with the decoded payload
The proving system answers: was this witness consistent with the circuit under this verifying key?
The verifier contract and its wrapper have to answer everything else:
- Are these the public inputs the product meant to accept?
- Has this logical transfer already been settled?
- Who can change the key, the circuit version, or the allowed message types?
- What can a valid-but-malicious payload still do once
verifyreturns true?
Those questions live in Solidity (or the destination VM), not in the constraint system. That is why "we audited the circuit" is an incomplete clearance for a ZK bridge audit.
Failure modes we keep finding
### 1. Public inputs that do not bind the economic event
The circuit proves something. The wrapper decides what that something is.
Gaps show up when:
- Amount, token, recipient, or source chain are only partly constrained as public inputs
- Critical fields are private witnesses that never appear in the onchain check
- Encoding is ambiguous (padding, optional fields, non-canonical serialization)
- The wrapper hashes a subset of fields, then trusts the rest from calldata
- Chain ID or bridge instance ID is missing, so a proof context can be reused across deployments
A sound proof over weak public inputs is still a sound proof. It may authorize the wrong transfer.
Audit question: for every field that moves value or authority on the destination chain, is it bound in the public input vector the wrapper actually checks?
### 2. Nullifiers and replay that only look solved
Replay protection is often a nullifier, message hash, or (srcChain, nonce) slot.
Problems:
- Nullifier derived from incomplete identity (same economic action, two nullifiers)
- Domain separation missing across chains, versions, or message types
- Refund / unwind paths that reuse identifiers with deposit paths
- Proof verified in one function, nullifier written in another, with a window between them
- Storage that marks "used" only after external calls succeed, inviting reentrancy races
The circuit may enforce "one witness, one nullifier." The wrapper still has to store and check it atomically with settlement. Cross-chain message verification fails here for the same reason non-ZK bridges fail: message identity is not stable.
### 3. Verifying key and circuit upgrades
Production bridges rarely freeze the verifying key forever. They add circuits, rotate setups, or hot-fix message versions.
Then you get:
- Admin or multisig that can point settlement at a new key without the same review bar as the original launch
- Multiple live keys with unclear which message types each accepts
- A "temporary" emergency key that stays authorized
- Upgrade paths that do not invalidate in-flight nullifier domains
- Proxy patterns where the verifier address moves while callers still assume the old public-input layout
If an attacker (or a compromised key holder) can install a verifying key for a circuit that accepts weaker public inputs, the original circuit audit is historical. Key rotation is a product control. Treat it like an upgrade of the bridge itself.
### 4. Light-client inputs treated as free trust
Some ZK bridges prove a light-client transition or sync-committee update, then treat the proven header as gospel for all subsequent messages.
That only holds if:
- The proven state root is the one the product thinks it is
- Finality assumptions on the source chain match what the destination already minted against
- Header / slot / period fields cannot be swapped under an equivalent proof
- Relayers cannot stall honest updates while pushing a preferred fork view the circuit still accepts
- Message proofs under that header cannot claim events that were never included
The wrapper is where those header fields become bridge policy. Mis-bound headers look like cryptography problems. They are usually binding problems.
### 5. Settlement after verify returns true
require(verifier.verify(proof, inputs)) is not the end of the control-flow graph.
After a valid proof, handlers still:
- Map assets across chains
- Apply decimals and custody inventory checks
- Call external adapters, DEXes, or messaging modules
- Credit balances under
msg.sender == bridgeassumptions - Emit events that offchain indexers treat as final
We keep seeing wrappers that verify correctly, then decode an arbitrary token and amount into a handler with no inventory invariant. The proof was fine. The economics were not.
This is the same class as receiving-handler over-trust in non-ZK bridges. Zero-knowledge proofs do not remove it. They only change how the message arrives.
### 6. Proof malleability and submitter games
Depending on the scheme and the wrapper:
- Different proof bytes may verify for the same public inputs
- Relayers choose which valid proof lands first
- Fee markets and private submission paths decide whether an unlock is starved while a mint proceeds elsewhere
- Batch verify paths that short-circuit on the first success without isolating failure domains
If settlement keys only on public inputs (good) but side effects depend on msg.sender or proof blob identity (bad), submitter preference becomes part of the trust model. ZK does not erase relayer privilege. It changes what the relayer is allowed to forge.
How this shows up in a review
When a ZK bridge engagement is scoped as "circuit only," the findings that later matter in production are usually out of bounds by design.
A more honest perimeter includes:
- The onchain verifier and every contract that calls it
- Public input encoding tests, not just circuit unit tests
- Nullifier storage and reentrancy around settlement
- Admin paths that change keys, circuits, or paused states
- Handler invariants after a successful verify
- Relayer and prover operational assumptions written down as trust, not marketing
If the report never opens the Solidity that runs after verify, it is not a ZK bridge security review. It is a proving-system review with bridge branding.
A practical review boundary
When we scope a ZK bridge, the diagram goes past the circuit repo:
- Public input map: every field that affects value or authority, and how it is encoded onchain
- Nullifier / replay store: derivation, domain separation, atomicity with settlement
- Verifying key lifecycle: who can rotate, multi-key policy, circuit versioning, emergency paths
- Header / state binding: what light-client or state proof actually authorizes downstream messages
- Post-verify handlers: asset mapping, inventory, decimals, external calls, reentrancy
- Ops: who runs provers and relayers, how proving keys and ceremony artifacts are held
If any row is "the ZK team owns that," it still decides whether your inventory moves.
Closing note
ZK makes a class of bridge lies expensive to forge. It does not make the onchain interpreter of a valid proof correct by default.
The proof can be sound. The contract that checks it still decides what gets through.
Teams that only review circuits will keep signing off on bridges that fail in the wrapper: weak public inputs, soft replay, key rotation theatre, and handlers that treat verify == true as a blank check. Teams that treat the verifier contract as the product surface find those gaps before liquidity does.
AN3 Intel · field notes on cross-chain proof systems and settlement risk.