返回 Expert 笔记
Expert Day 242

ZK 安全审计 — 3 份真实审计报告深度阅读

ZK 漏洞分类、underconstrained 模式、读 3 份真实 audit report

2026-12-29
Phase 4 - ZK电路开发实战 (Day 223-243)
ZKauditsecurityunderconstrainedsoundness

日期: 2026-12-29 方向: ZK工程 / 电路开发 阶段: Phase 4 - ZK电路开发实战 (Day 223-243) 标签: #ZK #audit #security #underconstrained #soundness


今日目标

类型内容
学习ZK 漏洞分类、underconstrained 模式、读 3 份真实 audit report
实操把每个漏洞案例用 Circom 重现 + 验证
产出zk_audit.md(漏洞分类 + 真实案例 + 自审 checklist)

ZK 漏洞分类(Top 10)

1. Underconstrained Circuit(最常见!)

症状:约束不足,prover 可以提交看似合理但违反业务规则的 witness。

典型例子:忘加 <==(用了 <--);没加 boolean 约束。

// BAD
template Bad() {
    signal input x;
    signal output y;
    y <-- x * x;        // 没约束 y = x*x
    // prover 可以填 y = 任意值
}

2. Non-deterministic in <--

// 场景: 模拟整除
signal q;
q <-- a / b;             // unsafe
// 应该:
q <-- a / b;
q * b === a;             // 约束 q*b = a

但即使加了约束,如果 b = 0,prover 仍可任填 q,因为 q*0 = 0 不约束 q。

3. Missing Range Check

整数运算可能 overflow 到 field:

signal sum;
sum <== x + y;           // x, y 都 < 2^30,但没保证 sum < p
// 如果 x = p-1, y = 1, sum = 0

4. Boolean Not Constrained

// BAD: s 不约束为 0/1
out <== s * a + (1-s) * b;
// prover 可填 s = 2 → out = 2a - b

5. Public Input 未参与运算

如果某个 public input 没出现在任何约束中,verifier 不会捕获它的篡改:

component main {public [hash, label]} = Foo();
// 但电路里只用了 hash,没用 label → label 可被任意篡改

6. Trusted Setup Incorrect

Phase 1 ptau 不够大、Phase 2 没做、用错 zkey。

7. Side-channel / Witness Leak

Witness 文件留在公网;prove 时间侧信道泄露。

8. Replay Attack

链上接受 proof 时没用 nonce / nullifier 防重放:

function withdraw(proof) public {
    verifier.verify(proof);     // 可重复调用!
    transferFunds(...);
}

9. Malleability of Public Inputs

某些 proof system 中 public input 顺序错可让验证通过但对应不同业务(snarkjs pi_b endianness 经典问题)。

10. Prover-Verifier State Mismatch

例:Tornado 状态有 30 个 historical roots,但 prover 用了不在 history 的 root → 链上 verify 失败但社区可能误以为电路漏洞。


案例 1: Aztec ECNoir Audit by Trail of Bits (2023)

项目: Noir 标准库 + Barretenberg verifier 审计方: Trail of Bits 关键 finding:

Finding A: Underconstrained to_le_bits in std

std::field::to_le_bits(N) 早期版本:

// pseudo-code of broken impl
fn to_le_bits(x: Field, n: u32) -> [bool; n] {
    let bits = unconstrained get_bits(x, n);     // unconstrained!
    // forgot: assert sum(bits[i] * 2^i) === x
    bits
}

→ prover 可以填任意 bits 数组,绕过 range check。 → 影响:所有用 std::range 的电路 affected。 → 修复:commit 92ab1f5 加上 sum constraint。

Finding B: Recursion verifier 的 hash to curve

verifier 的 hash-to-curve 实现没正确 reject identity point。攻击者可构造让 verifier 接受非有效 group element 的 proof。

修复:增加 is_on_curve() + is_in_subgroup() 检查。

教训

  • 标准库的漏洞影响巨大(所有项目 affected)
  • "unconstrained" 块必须配对 assert
  • pairing-friendly curve 操作必须验 subgroup

案例 2: Tornado Cash Trusted Setup (2019-2020)

项目: Tornado Cash 1 ETH pool 审计: ABDK Consulting + community

关键 finding

Phase 2 ceremony 未达 100 名参与者目标

只 1100 人参与(达标),但其中部分参与者使用同一 IP / 设备,引起对 entropy 的怀疑。 → 建议:未来 ceremony 应有 attestation 证明每个 contributor 用独立硬件。

Verifier.sol 的 pi_b 顺序

snarkjs 输出 pi_b 是 (c1, c0) 顺序,但 EVM precompile 期望 (c0, c1)。如果 dApp 不正确 swap,proof 永远 verify fail(DoS)但无法盗钱。 → 教训:永远用 snarkjs zkey export soliditycalldata 生成 calldata。

Withdraw recipient binding

如果 recipient 不绑定到 proof,relayer 可换地址。Tornado 把 recipient/relayer/fee/refund 都作为 public inputs(即使不参与电路计算,作为 dummy quadratic constraint 引入)。 → 教训:每个 public input 必须在电路中至少出现一次。

MerkleTreeWithHistory.sol 的 zero subtree hash

ZERO hash 用 keccak("tornado-cash-zero"),但实际计算时把它当 field element(mod p)。如果 mod 后的值碰巧等于某个真实 commitment,攻击者可 deposit 假 commitment 让 history 错位。 → 修复:检查 ZERO 不在用户提交的 commitment range 内。


案例 3: zkSync Era Boojum (Halborn Audit 2023)

项目: zkSync Era 的 Boojum 升级 审计: Halborn

Finding A: Goldilocks field underconstrained subtraction

Boojum 用 Goldilocks (p = 2^64 - 2^32 + 1)。某 subtraction gadget 在 underflow 时未正确 wrap:

correct: a - b  if a >= b
        p - (b - a)  if a < b

原实现忘了 underflow case,prover 可让结果 = -1(在域内是 p-1)通过。 → 修复:commit xxx,加 conditional add of p。

Finding B: Recursion AP-style preprocessor

zkSync 的 recursion 用 multi-layer wrapping。某 layer 的 verifier 在序列化 G2 element 时 endianness 错,让 inner proof bypass。 → 修复:明确 byte order convention + test vector。

Finding C: state root override

zkSync executor contract 在某 fork case 接受 prover 提交的 state root 但没验证 prior state root chain。 → 修复:增加 prior root check。

教训

  • 不同 field(Goldilocks vs bn254)每种都有 subtle 边界
  • 多层 recursion 是漏洞高发区(每层都要审)
  • 链上 contract 的 state root 校验是「最后一道防线」必须严格

自审 Checklist / Self-Audit Checklist

写电路时

  • 每个 <-- 后是否有对应 === 验证?
  • 每个 boolean signal 是否加 s * (1-s) === 0
  • 每个 integer 是否做 range check?
  • 是否处理了除数 = 0 的 case?
  • 每个 public input 是否在电路里至少出现一次?
  • 是否有 unconstrained 输出 signal?

Trusted Setup

  • Phase 1 ptau 大小是否覆盖电路约束数?
  • Phase 2 ceremony 是否有足够参与者?
  • zkey beacon hash 是否公开记录?
  • 是否所有 contributor 公开 attestation?

Solidity Verifier

  • pi_b 顺序是否用 soliditycalldata 生成?
  • verifier.sol 是否与电路 vkey 一致?
  • gas 消耗是否在合理范围?
  • 是否有 nullifier / nonce 防重放?
  • 是否对所有 public inputs 做 sanity check?

业务逻辑

  • recipient / fee / time 是否绑定到 proof?
  • 是否所有 state transition 的安全前置条件都被电路验证?
  • 是否区分 <=====?
  • hash 用 ZK-friendly 还是 Solidity 友好?两边一致吗?

ZK 审计公司 / Top ZK Auditors

公司专长知名 audit
Trail of Bits通用 + ZKNoir, Aztec, Worldcoin
Halborn协议 + ZKzkSync, Polygon zkEVM
Veridise专业 ZK + 形式化Aleo, Polygon, Scroll
0xPARC ZK Audit学术 + 创新Semaphore, Dark Forest
Spearbitcrowdsourced多个 ZK 项目
Least Authority老牌 ZKZcash, Filecoin

ZK 审计 cost:$50k - $500k 一次,依电路规模和复杂度。


形式化验证工具 / Formal Verification Tools

工具范围
Picus (Veridise)检测 Circom 的 underconstrained
Coda (UCSB)Circom DSL 形式化
Ecne (0xPARC)underconstrained detection
Halo2 analysis toolsPSE 内部

Picus 已经在多个 audit 中找到 underconstrained bug,是 ZK 自动化审计前沿。


真实漏洞总数(2020-2024)

公开 ZK audit 报告中:

  • 高危 finding: ~30%
  • 中危: ~40%
  • 低危/info: ~30%
  • 最常见类别: underconstrained (40%) > range check missing (15%) > replay (10%)

关键速查

ZK audit 三大问 (mnemonic)
  1. "Is every signal constrained?"   → underconstrained
  2. "Is every public used?"          → public input integrity
  3. "Is the verifier replay-safe?"   → contract layer

Tools:
  Picus      — find underconstrained
  Ecne       — alternative
  Halmos     — Solidity verifier check

面试题

  1. Q: ZK 电路最常见的漏洞类型? A: Underconstrained(约束不足)。表现:开发者用 <-- 给 signal 赋值但忘了加 === 或 boolean constraint,让 prover 可以提交不符合预期的 witness。统计上占 ZK audit findings 40% 以上。

  2. Q: Trusted Setup ceremony 出问题的最大风险是什么? A: 如果 Phase 2 的 toxic waste 被任何参与者保留(且没有 destroy),该参与者可以 forge 任意 proof,盗取协议所有资金。所以多人参与(1-of-N 诚实即安全)+ 公开 attestation 极重要。Tornado 用 1100 人,Filecoin/Aztec 都有大型 ceremony。

  3. Q: 如何审计一个 Circom 电路? A: (1) 读代码:每个 signal 是否被约束?(2) 用 Picus / Ecne 自动检测 underconstrained;(3) 写测试:包括 happy path + negative test (intentionally bad witness);(4) 检查 trusted setup 流程;(5) 审 Solidity verifier wrapper 防 replay;(6) 比对 ZK 内 hash 与 Solidity 端 hash 实现一致;(7) gas 估算合理。

  4. Q: 你给一个新 ZK 项目做 audit 检查清单,前 3 项是? A: (1) 每个 <-- 后必须有对应 === 验证(用 grep 全局检查);(2) 每个 public input 必须在电路中至少出现一次(防止被任意篡改);(3) Solidity verifier 必须有 nullifier / nonce 防重放(最大半数审计漏掉这一层)。


明日预告

Day 243 — Week 36 复习:ZK 工程能力整合。Phase 4 实战阶段(Day 223-243)的最终总结,把 21 天技能整合成「ZK 工程师能力图谱」。