Solidity Verifier Contract - Security Checklist
- Public-Input Bound Checks
- Proof-Element Validation Before Cryptographic Operations
- Correct Pairing Implementation and Verification-Key Consistency
- Gas Costs and Loop Structure
- Solidity Versioning and Language Safety Features
- Testing, Fuzzing, and Robust Negative Cases
- System-Level Context: The Verifier Is Only One Layer
Zero-knowledge proof (ZKP) verifier contracts — typically used in zk-SNARK or zk-proof systems — play a fundamental role in ensuring that proofs submitted on-chain are valid.
Although the cryptography behind ZKPs is strong, real-world vulnerabilities have frequently originated from:
- incorrect implementation,
- missing bound checks,
- or improper integration with higher-level smart-contract logic.
This article summarizes the essential points to verify when reviewing a Solidity verifier contract.
This article is mainly written by ChatGPT and the references used with a few edition of my part.
[TOC]
Public-Input Bound Checks
Every public input (often called public signals) must be validated to lie within the appropriate scalar field. For most SNARK constructions (e.g., BN254-based Groth16), each public value must satisfy:
0 ≤ value < r
Failing to enforce this constraint can allow malicious provers to exploit field overflows or bypass circuit constraints. This has been repeatedly highlighted in both practical security write-ups and academic work.
A secure verifier must explicitly check these bounds before using signals in linear combinations or cryptographic operations.
Details
In zk-SNARKs (such as Groth16 on BN254), all arithmetic inside the circuit happens in a finite field, typically denoted: \({F}_r\) where:
- r is a very large prime number,
- all public inputs, witness values, and circuit constraints must lie within this field,
- all computations are done modulo r.
So the condition:
0 ≤ value < r
means:
- The value must be a valid element of this finite field.
- Any number ≥ r would be interpreted incorrectly (modular reduction), which could break the security assumptions.
Details
Curve coordinates live in the base field q (check coordinates < q), while public scalar inputs are elements of the scalar field r (check 0 ≤ pub < r). Confusing these two fields is a common and dangerous mistake.” xn–2-umb.com+1
Proof-Element Validation Before Cryptographic Operations
Verifier contracts typically pass elliptic-curve points into EVM precompiles:
- ECADD (0x06): Elliptic Curve Addition
- ECMUL (0x07): Elliptic Curve Multiplication
- Pairing (0x08): Elliptic Curve Pairing
ECADD and ECMUL are defined in the EIP-196 and Pairing in EIP-197
These precompiles expect well-formed points whose coordinates lie in the base field. Many autogenerated verifiers rely on precompiles to fail naturally when inputs are malformed, but auditors often recommend additional explicit checks for clarity and to avoid undefined behavior.
Recommended validations include:
- Coordinates of A, B, C < base field modulus q
- Arrays have correct shape and order
- No zero or invalid points used where not allowed
Such preconditions lead to clearer, safer reverts and more deterministic behavior across chains.
See also RareSkills - Ethereum precompiled contracts and RareSkills - Elliptic Curves over Finite Fields
Details
“recompiles expect well-formed points, but their exact behavior (curve membership vs subgroup checks) depends on the chain and precompile version. Do not rely on failure semantics alone — prefer explicit, deterministic Solidity checks when portability is required.” ZK Nation Forum+1
Correct Pairing Implementation and Verification-Key Consistency
Cryptographic correctness is essential. The correctness of the verifier depends entirely on the consistency between:
- The verification key embedded in the contract
- The ZKP system parameters
- The circuit proving key
- The trusted setup (for SNARKs that require one)
Things to confirm:
- All verification-key constants (α, β, γ, δ, and IC points) match the expected parameters exactly.
- The linear combination
vk_x = IC0 + Σ(pub[i] * IC[i])is implemented correctly. - Negation of
Ais properly applied if required by the protocol. - The order and layout of pairing arguments match the specification of the ZKP scheme.
Small deviations — such as swapped x/y coordinates, wrong G1/G2 ordering, or missing negation — break the verifier
Verification-Key Constants Must Match Exactly
The verifier hardcodes the elliptic-curve points that define the verification key (VK):
- α in G1
- β, γ, δ in G2
- IC[0..n] in G1
These values must match exactly the ones generated by the trusted setup (.zkey).
If even one coordinate, one index, or one field element is wrong, one of two things happens:
A. Proofs never verify
System becomes unusable.
B. Invalid proofs verify (critical vulnerability)
This happens when VK constants are structurally wrong or out of order, e.g.:
- IC indices shifted
- mis-copied coordinates
- swapped G2 components
- VK from a different circuit
This enables cross-circuit verification: proofs for a different or weaker circuit verify on-chain.
How to check it
- Export vk.json from
snarkjs zkey export verificationkey. - Normalize affine coordinates.
- Compare every coordinate byte-for-byte with Solidity VK.
- Ensure IC[i] order matches publicSignals order exactly.
Correctness of the Linear Combination vk_x = IC0 + Σ(pub[i]·IC[i])
The verifier computes the G1 point: \(vk_x=IC[0]+∑ipub[i]⋅IC[i+1]\) This binds the public inputs to the proof. Any mistake here breaks soundness.
What can go wrong
- Wrong IC index (off-by-one).
- Wrong ordering of public signals.
- Missing check
pub[i] < r, allowing wraparound. - Incorrect ECMUL/ECADD calls in assembly.
- Wrong memory offsets in assembly.
- Mixing base field q with scalar field r.
Why it matters
If vk_x is computed incorrectly, the pairing equation can become satisfied even for invalid proofs, allowing attackers to bypass circuit constraints.
How can you check it
- Recompute vk_x off-chain.
- Compare with on-chain result using instrumentation.
- Fuzz input ordering to ensure mismatches cause verification failure.
- Ensure every public input is validated:
0 ≤ pub[i] < r.
Gas Costs and Loop Structure
While verifiers are generally static and predictable, some implementations iterate over public inputs or verification-key entries. Key points:
- Ensure the number of inputs is strictly bounded.
- Avoid user-controlled loops or unbounded iteration.
- Consider the gas implications on the target network (L1 vs L2).
- For large public-input vectors, consider alternatives such as recursive proofs, compression, or verifying aggregated proofs.
Solidity Versioning and Language Safety Features
Security-sensitive contracts should use modern versions of Solidity (0.8.x), which provide:
- Automatic overflow/underflow protection
- Improved error handling
- Better static-analysis support
- Fewer compiler edge-case bugs
Autogenerated verifiers sometimes specify broad pragmas (e.g., >=0.7.0 <0.9.0). It is safer to pin them to a hardened major version such as:
pragma solidity ^0.8.20;
This ensures predictable compiler behavior and reduces attack surface.
Testing, Fuzzing, and Robust Negative Cases
A good verifier must not only accept valid proofs but also reliably reject invalid submissions. Test coverage should include:
- Valid proofs (generated freshly after each setup)
- Corrupted proof elements (modified A, B, C)
- Public inputs out of range (≥ r)
- Out-of-curve point inputs
- Random large data values
- Reordered proof elements
- Gas-limit edge cases
Fuzzers should be run not just on the Solidity contract but also on the client-side proof generation pipeline, since mismatches frequently arise there.
Details
Test pub = r, pub = r+1, pub = r-1. Also test proofs where proof points are valid base-field elements but not in the required subgroup (if chain doesn’t enforce subgroup). oxor.io+1
System-Level Context: The Verifier Is Only One Layer
Even a flawless verifier does not guarantee system integrity. Common systemic issues include:
- Under-constrained circuits (the prover can “prove” something incorrect)
- Business-logic vulnerabilities in contracts that consume the verifier
- Incorrect assumption that verification implies authorization
- Replay attacks when proofs lack uniqueness constraints
- Incorrect hashing/commitment logic around public signals
Therefore, verify:
- The intended semantics are fully encoded in the circuit
- On-chain state transitions are correctly tied to circuit constraints
- Proofs cannot be reused in unintended contexts
- The full protocol follows defense-in-depth principles
References
- arXiv - SoK: What Don’t We Know? Understanding Security of SNARK-Based Systems
- arXiv - Zero-Knowledge Proof Frameworks: A Systematic Survey
- OXORIO - Common Vulnerabilities in ZK Proof
- Crypto StackExchange - What is required to verify a zk-SNARK?
- ScienceDirect - A Survey of Smart Contract Security
- Medium - Solidity Security Best Practices
- docs.circom.io - proving-circuits/
- ChatGPT to summarize the different resources and articles