Public-Input Domain Mismatch / Field-Modulus Aliasing
- Name: public-input domain mismatch / field-modulus aliasing
- Category: exploit pattern / zk-proof integration failure / finite-field boundary mismatch / raw-uint256 versus field-element drift
- Summary: A zk verifier only reasons about public inputs inside its scalar field, but surrounding contracts often store or compare those same values as unrestricted
uint256s. If the application does not enforce the same< qboundary that the proof system assumes, the verifier and the contract stop talking about one canonical object. Two different 256-bit values such asxandx + qcan collapse to the same field element inside proof verification while still looking distinct to a Solidity uniqueness map, a user-facing identifier likegroupIdcan be accepted by contract code even though the verifier will always reject it, or a Merkle tree can be initialized with zero values that later become unusable proof siblings. The result can be replayable nullifiers, double-signaling, duplicate reward claims, permanently unverifiable groups or polls, or Merkle / commitment objects that can never participate soundly in the proof flow they were created for. - What it is:
- A zk application feeds security-critical public inputs from Solidity into a verifier generated for a finite field of size
q. - The verifier expects each public input to live in that field, whether by explicit
input[i] < qchecks or by arithmetic that is only meaningful moduloq. - Application contracts, storage maps, or helper modules treat the same values as unrestricted
uint256s. - The protocol implicitly assumes
field element seen by the verifierandraw integer consumed by the contractare the same object. - In the vulnerable case, no end-to-end range check or canonicalization forces that equality of meaning.
- The exploit cashes out wherever the raw-value surface matters: nullifier uniqueness, scope selection, external nullifier / group identifiers, Merkle-tree initialization, or any other proof-facing state object.
- A zk application feeds security-critical public inputs from Solidity into a verifier generated for a finite field of size
- Typical preconditions:
- Proof-facing values such as nullifiers, group ids, external nullifiers, commitments, roots, or zero values enter the system through Solidity
uint256parameters. - The verifier or circuit only makes sense for values in the SNARK scalar field.
- The surrounding contract stores, compares, or indexes those values without first range-checking or canonicalizing them.
- The same value matters both to proof validity and to an onchain uniqueness / authorization / availability rule.
- Successful proof verification or successful object creation directly unlocks some economically or operationally meaningful path.
- Proof-facing values such as nullifiers, group ids, external nullifiers, commitments, roots, or zero values enter the system through Solidity
- Common vulnerable surfaces:
- nullifier maps where the verifier sees values modulo
qbut Solidity stores rawuint256keys - external-nullifier, scope, or group-id fields reused as public inputs in voting, signaling, or anonymous-action systems
- Merkle-tree initialization paths that accept leaf or zero values larger than the proving field even though later inclusion proofs, removal proofs, or sibling checks assume field-safe inputs
- precomputed default-path state such as zero-leaf ladders, empty-subtree caches, or constructor-seeded commitment tables that quietly become proof siblings later even if callers think of them as harmless setup data
- helper contracts and extensions that inherit zk primitives but omit the same scalar-field checks as the base verifier
- legacy proof integrations where developers remember the verifier’s field constraint abstractly but do not push that invariant out to every caller-facing contract boundary
- nullifier maps where the verifier sees values modulo
- Exploit sequence:
- The attacker identifies a proof-facing value that the application accepts as an unrestricted
uint256. - They find where the proof system interprets that value inside the SNARK field, either by explicit
< qchecks or by arithmetic that effectively reduces moduloq. - They choose two raw values that the contract distinguishes but the proof system does not, or one raw value that the contract accepts even though the verifier will always reject it.
- In the replay variant, they use the aliased value to satisfy proof verification while bypassing raw-value uniqueness checks such as
usedNullifier[x]. - In the availability variant, they create a group, poll, tree, or scope that looks valid to contract code but can never verify later proofs.
- The protocol either pays out, accepts a duplicate anonymous action, or silently bricks the proof-backed object.
- The attacker identifies a proof-facing value that the application accepts as an unrestricted
- Variants:
- Nullifier alias replay —
nandn + qverify as the same field element, but onchain uniqueness maps treat them as different raw values, enabling double-use of one proof right. - Scope / group-id denial of service — a poll, group, or external nullifier is created above the scalar-field bound, so future proofs always revert even though the object was accepted onchain.
- Commitment / leaf initialization mismatch — Merkle or commitment values outside the field are admitted into state, so later proof generation or verification breaks or becomes unsound.
- Extension-level omission — the base protocol understands the field bound, but downstream voting, signaling, or wrapper contracts forget to enforce it at their own caller boundary.
- Nullifier alias replay —
Three proof-facing input promotions healthy systems should not collapse
-
A raw proof-facing value exists at all
- A caller supplied a
uint256, a constructor accepted agroupId, a tree was initialized with a zero value, or a wrapper forwarded a nullifier-like identifier. - This is only the transport-shape question:
some raw value reached the proof-facing boundary.
- A caller supplied a
-
That raw value counts as the canonical field-safe object the proving system means to reason about
- This is the promotion vulnerable systems skip over. They quietly treat
the contract accepted this integeras if it already meantthe verifier, storage layer, and later proofs all agree on one canonical field element. - Semaphore’s
groupId/ external-nullifier bug is the clean availability-side example: a raw identifier existed and was accepted onchain, but that never made it a field-safe object the verifier could honor later.
- This is the promotion vulnerable systems skip over. They quietly treat
-
That accepted field-safe object deserves downstream uniqueness, availability, or payout authority
- Even once an input is properly canonicalized into one field-safe object, the protocol still has to decide what that object may authorize: one-time claims, anonymous signaling, poll participation, tree usability, or reward payout.
- Healthy systems do not let
this integer arrivedor eventhis integer is field-safestand in forthis object now deserves replay-prevention, scope-selection, or economically meaningful authoritywithout making those later promotions explicit.
- Detection heuristics:
- Trace every public input from user-controlled Solidity parameter to verifier boundary and ask where
< qis first enforced. - Fuzz with
x,x + q, andx + 2qfor nullifiers, scopes, group ids, leaves, and commitments; require the system to treat them consistently. - Audit storage keys and uniqueness maps for proof-facing values that are keyed by raw
uint256rather than canonical field-safe representations. - Review extension contracts, not just the base verifier, because many incidents come from wrappers that reuse proof-facing identifiers without reusing scalar-field guards.
- For availability-sensitive flows, try creating groups, polls, or trees at the largest possible
uint256values and confirm proofs remain generatable and verifiable. - Treat
verifier rejects out-of-range inputsas only half a defense; the rest of the system must reject or canonicalize those same inputs before they become state.
- Trace every public input from user-controlled Solidity parameter to verifier boundary and ask where
- Defenses:
- Enforce
input < qat the earliest caller-facing contract boundary for every proof-facing value, not only inside the verifier. - Canonicalize public inputs once and use that canonical representation for storage, uniqueness maps, events, and downstream comparisons.
- Make nullifier-consumption logic and proof verification talk about the same domain object: one field-safe nullifier, not one verifier-side field element plus one raw storage key.
- Reuse shared scalar-field guard libraries across base contracts and application extensions so wrappers do not silently drop the bound.
- Add invariant tests that prove availability too: any created group, poll, or tree must admit at least one syntactically valid proof path if other preconditions are met.
- Document the proving field as part of the protocol’s external API, not as an internal verifier detail.
- Enforce
What sound design looks like
- Healthy systems keep three facts separate on purpose:
a raw proof-facing value exists,that value is the canonical field-safe object the proving system actually reasons about, andthat accepted object is now allowed to drive replay-prevention, proof-backed object creation, or payout authority. - A proof-facing identifier should have one machine-checked domain all the way from user input to verifier logic. If a nullifier, scope, group id, zero value, or commitment is meaningful only as an element of the BN254 scalar field, then the contract that first accepts it should enforce that fact before it ever becomes storage, event data, or reward-consumption state.
- Uniqueness and proof validity should share the same canonical object. A healthy design does not let the verifier consume
n mod qwhile the replay-protection map stores rawnandn + qas different keys. - Availability-sensitive objects like groups, polls, and Merkle trees should be validated at creation time against the proving system’s own domain assumptions.
Object was createdshould already implythis object can later participate in the proof flow it was created for. - Library defaults deserve the same treatment as explicit user inputs. If zero leaves, empty-subtree hashes, or seeded commitment tables are ever going to reappear inside Merkle proofs or public-input vectors, they are already proof-facing objects and should be forced into the field before initialization succeeds.
- The clean reviewer question is:
where exactly do we prove that the Solidity value we stored, compared, or keyed is the same field element the verifier later reasons about, and where exactly do we prove what downstream authority that accepted field-safe object is allowed to carry? - High-signal incidents:
- double-signal replay near miss (2019-07-25) — the replay variant, where raw nullifier keys could diverge from verifier-side field semantics and allow the same anonymous action right to be consumed twice.
- [[../../../incidents/proofs-verifiers/a16z-zkdrops-privateairdrop-nullifier-range-check-double-claim-near-miss-2022-01-26|a16z ZkDrops
PrivateAirdrop.collectAirdrop()nullifier range-check omission / double-claim near miss (2022-01-26)]] — the claim-payout replay variant, where one Merkle airdrop entitlement could be redeemed twice by presentingnandn + qas distinct onchain nullifiers. - [[../../../incidents/proofs-verifiers/semaphore-groupid-external-nullifier-range-check-omission-inoperable-poll-near-miss-2022-03-07|Semaphore
groupId/ external-nullifier range-check omission / inoperable poll near miss (2022-03-07)]] — the availability variant, where contracts accepted identifiers the verifier would never honor. - [[../../../incidents/proofs-verifiers/zk-kit-incremental-binary-quin-tree-zero-leaf-snark-scalar-range-check-omission-near-miss-2022-03-09|ZK-Kit incremental binary/quin tree zero-leaf
SNARK_SCALAR_FIELDrange-check omission near miss (2022-03-09)]] — the Merkle-initialization variant, where the impossible object was not a replayable nullifier or oversized poll id but a proof-backed tree seeded with out-of-field zero values that later reappeared as invalid siblings.
Failure-prevention checklist
- Enforce the proving-field bound at the first caller-facing boundary for every proof-facing value: nullifiers, scopes, group ids, external nullifiers, roots, zero values, leaves, and commitments. Do not let raw transport-shape acceptance masquerade as proof-facing validity.
- Canonicalize proof-facing identifiers once and reuse that canonical form everywhere: verifier inputs, storage keys, replay-protection maps, emitted events, and downstream authorization logic.
- Keep object-creation rules aligned with proof-verification rules. If a group, poll, tree, or commitment object would later be unverifiable because a value is out of field, reject it before state is created.
- Centralize scalar-field guards in shared libraries or modifiers so wrappers, extensions, and helper contracts cannot silently accept a wider domain than the base verifier.
- Add adversarial tests for
x,x + q, andx + 2qacross every replay-sensitive or availability-sensitive input surface. Require the system to reject or treat them identically, never inconsistently. - Make uniqueness maps key the same semantic object the verifier consumes. A nullifier-spent map should not index raw
uint256values if the proof system only recognizes field-canonicalized elements. - Validate Merkle tree initialization parameters, zero values, empty-subtree ladders, and helper defaults against the proving field at creation time, not only when proofs later fail.
- Document the proving field and canonicalization rules as part of the external contract API so integrators understand that
uint256is only the transport type, not the semantic domain. - Include availability invariants in testing and monitoring: any created group, poll, or tree should still admit valid proof generation under ordinary conditions.
- Review downstream wrappers and governance-added extensions with the same paranoia as the original verifier integration; range-check omissions often reappear one layer out from the audited core.
Audit prompts
- Where is
< qfirst enforced for each proof-facing value, and does every caller path hit that guard before state, events, or replay-protection maps are updated? - Do storage keys, uniqueness maps, and emitted identifiers use the same canonical field-safe representation the verifier reasons about, or raw
uint256values that can alias moduloq? - Can an attacker supply
x + qto consume the same proof right, nullifier, or anonymous action while bypassing an onchain uniqueness check keyed by raw integer value? - Can the contract create a group, poll, tree, or other proof-backed object using identifiers or zero values that the verifier will later reject, permanently bricking the object?
- Do extension contracts, wrappers, or helper modules reuse proof-facing ids without reusing the same scalar-field guardrails as the base system?
- Are there any places where a verifier-side rejection is relied upon as the only protection, even though state changes or object creation happen earlier?
- What exact canonicalization rule would an offchain prover, indexer, or relayer need to reproduce so that it talks about the same identifier the contract stores?
- If one identifier is emitted in logs, stored in mappings, and passed into proofs, are those three representations provably the same semantic object?
- During upgrades, where is the assertion that the new circuit field, verifier wrapper, and application-layer storage rules still agree about input domain?
- If a user asks why a created poll, group, or tree can never verify a proof, what creation-time invariant failed to stop that impossible object from existing?
Lessons that should already be standard
uint256is not a semantic domain. In zk integrations it is often just a transport envelope for a much smaller field element, and treatingraw integer arrivedas if it already meantcanonical proof-facing object existsis how replay and bricking bugs happen.- Replay protection only works if the replay-protection map and the verifier consume the same canonical identifier. Raw-value uniqueness is not enough when the proof system aliases modulo
q. - Availability is part of correctness. Accepting proof-backed objects that can never later verify is a real protocol failure, not a mere UX bug.
- Verifier-side range checks are necessary but not sufficient. By the time the verifier rejects an out-of-field value, the application may already have stored unusable state or emitted misleading external truth.
- Wrapper contracts are where sound invariants often die. Teams remember the field bound in the audited core and then forget to restate it in extensions, voting modules, airdrop claimers, or helper factories.
- The proving field belongs in external docs, code comments, tests, and API assumptions. If integrators have to rediscover
SNARK_SCALAR_FIELDfrom generated verifier code, the boundary is too implicit. - The clean mental model is
one proof-facing identifier, one canonical field-safe representation, everywhere. Any second representation that matters operationally is a likely exploit surface.
Where this pattern stops
- Use creation-time-proof-usability-failure when the sharper recurring story is no longer broad field-domain drift or replay, but that the application published a proof-backed object that was dead on arrival for its intended lifecycle. Semaphore oversized
groupIdpolls and ZK-Kit poisoned zero-leaf trees still belong to this broader family, but that specialized note carries the heavier analytical load whencreated successfully, unusable lateris the main lesson. - Use witness-public-input-binding-failure when the proof statement is wrong because public-input truth and witness-side truth were split into two unconstrained claims. That family is about an underbound statement inside the proof relation itself. This family is about the contract and verifier disagreeing on the domain of one public input.
- Use proof-to-effects-binding-failure when the proof may be sound and the public-input domain may be sound, but a downstream settlement consumer still executes a different economically relevant effect set than the proof authenticated.
- Use verification-key-independence-collapse when the verifier key itself destroys soundness for all inputs. Here the proving system can remain mathematically fine while the application mis-handles the boundary between field elements and raw contract integers.
- In short: this pattern begins when
the verifier's field elementandthe contract's stored integerstop being one canonical object, and it stops once the sharper story is instead a split statement, a bad proof consumer, or a broken verifier key.
Internal linkages
- Best current exemplars: semaphore-nullifier-field-modulus-aliasing-double-signal-replay-near-miss-2019-07-25, a16z-zkdrops-privateairdrop-nullifier-range-check-double-claim-near-miss-2022-01-26, semaphore-groupid-external-nullifier-range-check-omission-inoperable-poll-near-miss-2022-03-07, zk-kit-incremental-binary-quin-tree-zero-leaf-snark-scalar-range-check-omission-near-miss-2022-03-09.
- Specialized object-lifecycle subfamily: creation-time-proof-usability-failure — use it when the sharper analytical weight lives in
created successfully, unusable laterrather than in broad finite-field aliasing or replay. - Positive-design companion: proof-facing-field-domain-canonicalization-discipline — use it when the sharper question is where caller-boundary checks, storage canonicalization, and object-creation rules should force one field-safe proof-facing object before state exists.
- Positive-design companion: creation-time-proof-usability-and-dead-on-arrival-object-prevention — use it when the sharper question is why object publication was allowed to outrun later proof usability at all.
- Adjacent split-statement family: witness-public-input-binding-failure — there the circuit proves the wrong relation; here the verifier and contract disagree on the numeric domain of one public input.
- Adjacent proof-consumer family: proof-to-effects-binding-failure — useful when the public inputs were fine but the economic consumer later honored a different effect set.
Control surface
-
The leverage sits in caller-facing range checks, canonicalization of proof-facing identifiers, raw
uint256storage keys, object-creation paths, and any wrapper contract that reuses zk inputs without reusing scalar-field guards. -
The recurring mistake is to treat
SNARK field boundas a verifier implementation detail instead of an end-to-end API invariant, then to let raw-value admission quietly promote into canonical proof-facing truth and later into uniqueness, availability, or payout authority. -
Once proof validity and onchain uniqueness or availability disagree about the domain of one input, the system can be replayable or permanently unusable while every component still looks locally reasonable.
-
Sources:
-
Last reviewed: 2026-07-31 UTC