# Zebra: Missing copy constraint in halo2_gadgets variable-base scalar multiplication allows under-constrained base, breaking Orchard Action circuit soundness

Missing copy constraint in halo2_gadgets variable-base scalar multiplication let a malicious prover forge Orchard Action proofs, enabling undetectable double-spends and note theft.

- Payout: Research
- Severity: critical
- Program: Zcash Orchard
- Found by: defuse
- Disclosed: 2026-07-06
- CVE: CVE-2026-54496
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-ww9q-8r59-xv46
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-ww9q-8r59-xv46
- Tags: area:web3 domain:web3 impact:funds-loss impact:integrity kind:research platform:ghsa sev:critical tech:rust technique:source-review vuln:accounting-error vuln:crypto

**Target:** halo2_gadgets variable-base scalar multiplication gadget (Orchard Action circuit)
**Root cause:** The double-and-add loop assigned the base point via assign_advice() without a copy constraint tying it to the actual base; q_mul_2 only forced loop-internal equality, so a prover could choose an arbitrary base satisfying pk_d = [ivk] g_d.
**Steps:**
1. Read halo2_gadgets/src/ecc/chip/mul/incomplete.rs around L309-L310: `region.assign_advice(|| "x_p", self.double_and_add.x_p, row + offset, || x_p)?;` and `region.assign_advice(|| "y_p", self.y_p, row + offset, || y_p)?;`
2. Observe no copy constraint links the witnessed base to the real base; q_mul_2 only enforces equality across loop iterations.
3. Pick a fresh incorrect nk for the target note; derive the resulting wrong ivk honestly via the remaining constraints.
4. Solve for a base value making [ivk] g_d equal the desired pk_d, satisfying the diversified-address-integrity check.
5. Produce a valid Orchard Action proof with the forged nullifier; repeat with distinct nk values to double-spend the same note.
**How found:** Found during a protocol audit by Taylor Hornby (Shielded Labs): code review of the incomplete-addition stage spotted assign_advice() used for the base point where a copy constraint was needed, and reasoning that q_mul_2 only enforces loop-internal equality, leaving the base free.
**Exploitation:** A malicious prover sets private circuit inputs to chosen values and solves for an under-constrained base so the diversified-address-integrity equation pk_d = [ivk] g_d holds for an arbitrary (pk_d, g_d, ivk) triple. Using a wrong nk yields a distinct valid nullifier per spend, enabling repeated spends of one note (balance violation bounded by the turnstile). Alternatively, choosing an ak with known ask lets the attacker sign spends of a victim's notes whose plaintext/ivk they know, bypassing hardware-wallet spend-key protection.
**Escalation:** Repeating with distinct wrong nk values multiplies nullifiers for the same note, inflating value within the Orchard pool up to the turnstile limit; theft variant bypasses hardware-wallet key isolation when the linked software wallet is compromised.
**Insight:** In ZK circuits, audit every assign_advice() whose intended invariant is 'equals a known/fixed value' — a witness assignment without copy_advice() leaves that value prover-chosen; check that cross-iteration equality constraints actually anchor to the real input, not just to each other.

## Proof of concept (copied exactly from the original report)

**Vulnerable assign_advice calls for base point**

```
region.assign_advice(|| "x_p", self.double_and_add.x_p, row + offset, || x_p)?;
region.assign_advice(|| "y_p", self.y_p, row + offset, || y_p)?;
```

