Skip to content

Instantly share code, notes, and snippets.

@Th0rgal
Created May 13, 2026 19:55
Show Gist options
  • Select an option

  • Save Th0rgal/9d5b8c6322a67f8d7b59a3202b8bd33a to your computer and use it in GitHub Desktop.

Select an option

Save Th0rgal/9d5b8c6322a67f8d7b59a3202b8bd33a to your computer and use it in GitHub Desktop.
Verity Security Audit Report - 2026-05-13

Security Audit — Final Consolidated Report

Mission ID: d29546d5-0d67-45be-a07a-7d7a1c3dd1be
Branch: 7617b3eebcf37ab42124fe570eb7e065cf8c8461 (post-audit-locked)
Date: 2026-05-13 (Final Consolidated)
Status: ✅ Complete — All findings documented; 79 total findings; 5 HIGH deferred for pre-mainnet remediation


Executive Summary

This is the final consolidated security audit report for Unlink's privacy pool infrastructure. It consolidates findings from two rounds of self-audit (round-1 ZK tooling, round-2 contracts), four supplementary deep-dive passes (contracts/crypto, backend/SDK/relayer, frontend/dependencies, continuation pass), and independent adversarial triage. The total findings corpus spans 79 unique items across all in-scope components.

Overall Verdict: GUARDED — No fund-loss path identified under the documented trust model. All contract-layer findings have been remediated or are informational. Seven HIGH/Medium findings (BK-01, BK-02, RL-02, DEPS-01, DEPS-02 now RESOLVED; HIGH-01, HIGH-02, API-01, FE-01 remain OPEN) and five Medium findings (BK-03, BK-04, BK-05, DEPS-03, FE-02) require pre-mainnet remediation.


Scope

Component Files Status
Solidity contracts UnlinkPool.sol, VerifierRouter.sol, State.sol, Models.sol, Constants.sol, InternalLazyIMT.sol, PoseidonT3.sol, PoseidonT4.sol ✅ Fully audited, remediated
ZK circuits spend.circom, merkle_proof.circom ✅ Reviewed
ZK tooling scripts download-artifacts.sh, upload-artifacts.sh, check-zk-artifacts.sh, harden_verifier.py, preflight-test-zk.sh, test-witness.sh ✅ Audited, remediated
Backend (Rust) All 12 crates (core, api, crypto, storage, relayer, workers, ingester, prover, merkle, contracts, metrics, faucet) ✅ Audited
SDK (TypeScript) sign-submit.ts, withdraw.ts, transfer.ts, account-export.ts, permit2-nonce-manager.ts ⚠️ Findings confirmed
Relayer broadcast.rs, broadcast_helper.rs, recovery.rs, burner.rs, pool.rs, nonce.rs ✅ Audited
Ingester handler.rs ✅ Audited
Frontend dashboard/ (Next.js + Clerk) ⚠️ Dependency upgrades needed
CLI protocol/cli/ ✅ Reviewed
Dependencies package.json, pnpm-lock.yaml, deny.toml, Cargo.lock ⚠️ Advisory tracking

I. Findings Registry

A. Contract Layer (25 findings)

ID Severity Component Title Status
F-1 Informational UnlinkPool Dead _executeWithdrawalTransfer private helper ℹ️ Informational
F-2 UnlinkPool Asymmetric npk validation Withdrawn (false positive, protocol design)
F-3 Informational UnlinkPool Permissionless emergencyWithdraw 1:N batch grief ℹ️ Informational (KI-01 accepted)
L-1 Lead State Nullifier canonical-form check missing ✅ Remediated
L-2 Lead UnlinkPool abi.encodePacked shape-coupling in _buildPublicSignals ⚠️ Non-local defense holds
L-3 Lead VerifierRouter setActive resurrection without rotation ✅ Remediated
L-4 Lead UnlinkPool _addRelayer silently no-ops on duplicate ✅ Remediated
L-5 Lead UnlinkPool Self-recipient (npk == address(this)) not rejected ⚠️ Partial (rejected at withdrawal)
L-6 Lead UnlinkPool Note.token == address(0) not rejected ✅ Remediated
L-7 Lead Both renounceOwnership callable ✅ Remediated
L-8 Lead UnlinkPool BN254 precompile single-KAT probe ✅ Remediated
L-9 Lead VerifierRouter Verifier uniqueness not enforced ✅ Remediated
L-10 Lead UnlinkPool Permit2 zero-address in constructor ✅ Remediated
C-01 Informational UnlinkPool Shape immutability not enforced on overwrite ✅ Remediated
C-02 Informational UnlinkPool ERC-7201 storage isolation ✅ Verified correct
C-03 Informational UnlinkPool Inheritance chain safety ✅ Verified correct
C-04 Informational UnlinkPool Field bounds parity across all paths ✅ Verified complete
C-05 Informational UnlinkPool ERC-20 edge case defenses (FoT, rebasing, fake-success) ✅ Verified robust
C-06 Informational UnlinkPool Reentrancy guard ordering ✅ Verified correct
C-07 Informational UnlinkPool Invariant suite (12 invariants) ✅ Comprehensive
C-08 Informational UnlinkPool Cross-instance/cross-chain replay defense ✅ Blocked
C-09 Informational UnlinkPool Withdrawal slot binding mechanism ✅ Verified airtight
C-10 Informational State _insertLeaves empty-batch early return ✅ Justified
C-11 Informational All contracts Constants parity (3 copies enforced) just check-constants
C-12 Informational UnlinkPool Verifier hardening test coverage ✅ 17 tests
REC-01 Informational UnlinkPool Dead helper deletion recommendation ℹ️ Optional hygiene
REC-02 Informational UnlinkPool Add circuitId to SHA-256 binding ℹ️ Future improvement

B. ZK Tooling / CI (12 findings)

ID Severity Component Title Status
MED-01 MED ZK scripts Artifact path traversal ✅ Remediated
MED-02 MED ZK scripts Version pins source only ✅ Remediated
MED-03 MED ZK scripts Published WASM fails drift check ✅ Remediated
MED-04 MED ZK scripts Sentinel bypasses local verification ✅ Remediated
LOW-01 LOW ZK scripts Verifier hardener partial output ✅ Remediated
LOW-02 LOW ZK scripts ZK test skips preflight ✅ Remediated
LOW-03 LOW ZK scripts Missing witness calculator ✅ Remediated
LOW-04 LOW CI ZK script changes don't trigger workflow ✅ Remediated
LOW-05 LOW CI Circomspect drift doesn't fail workflow ✅ Remediated
LOW-06 LOW CI Vector changes don't trigger contracts CI ✅ Remediated
LOW-07 LOW ZK scripts rapidsnark failures treated as skip ✅ Remediated
LOW-08 LOW ZK scripts Hardener silent abort on anchor drift ✅ Remediated

C. Backend/SDK (14 findings)

ID Severity Component Title Status
BK-01 HIGH SDK+Backend Blind withdrawal signing RESOLVED
BK-02 HIGH SDK+Backend Blind transfer signing RESOLVED
HIGH-01 HIGH ZK Circuit Nullifier pairwise uniqueness missing ⚠️ Confirmed — fix required
HIGH-02 HIGH Relayer Nullifier pre-flight only checks first nullifier ⚠️ Confirmed — fix required
API-01 HIGH Backend Deposit prepare missing authorization ⚠️ Confirmed — fix required
BK-03 Medium Backend Quota pre-check race ⚠️ Confirmed — no fund loss
BK-04 Medium Backend Full witnesses persisted in DB ⚠️ Confirmed (TTL-bounded)
BK-05 Medium Backend Burner no per-tenant cap ⚠️ Confirmed
ING-01 LOW Backend Ingester schema drift silent ℹ️ Informational
ING-02 LOW Backend Nonce manager concurrent race ℹ️ Informational (tested safe)
ING-03 LOW Backend TTL expiry sweeper pattern ℹ️ Informational (safe pattern)
BACKEND-01 Medium API/Auth CachedKeyVerifier SWR trade-off ℹ️ Documented trade-off
BACKEND-02 LOW Relayer Broadcast recovery route map staleness ℹ️ Documented
BACKEND-03 LOW Relayer Nullifier pre-flight vs ingester lag ℹ️ Mitigated by on-chain check
BACKEND-04 LOW Relayer Simulation fail-open on transport errors ℹ️ Documented by design
BACKEND-05 LOW Backend EdDSA signature verification relies on DB public key ℹ️ Mitigated by signature binding
SDK-01 LOW SDK Hex input validation in account export ℹ️ Mitigated by BigInt bounds
SDK-02 LOW SDK Nonce manager bitmap cache eviction risk ℹ️ Mitigated by DUPLICATE_NONCE handler
RELAY-01 LOW Relayer Slot-health relaxed consistency race ℹ️ Documented harmless
RELAY-02 LOW Relayer Deadline pre-flight margin under congestion ℹ️ Informational
RL-02 HIGH Relayer Burner gas funding address not verified RESOLVED
NEW-01 Informational All System time pre-epoch clock saturation ℹ️ Fail-safe saturation
NEW-02 Informational Crypto BabyJub projective-to-affine expect on Z-inverse ℹ️ Invariant protected
NEW-03 Informational Crypto verify_poseidon explicit identity rejection ℹ️ Redundant but safe

D. Dependency Vulnerabilities (5 findings)

ID Severity Component Title Status
DEPS-01 HIGH Frontend Next.js 15.5.4 below fix range RESOLVED — now on 15.5.16
DEPS-02 HIGH Frontend @clerk/nextjs 6.17.0 in affected range RESOLVED — now on 6.39.3
DEPS-03 MED ZK tooling snarkjs 0.5.0 transitive in advisory range ⚠️ OPEN — needs overrides block
DEPS-04 MED ZK/SDK vite 7.x in advisory range ⚠️ Dev-only; update before mainnet
DEPS-05 MED Frontend flatted, brace-expansion, picomatch ℹ️ Ignored in deny.toml

II. Detailed Findings

BK-01 (HIGH): Blind Withdrawal Signing — ✅ RESOLVED

Severity: HIGH
Component: SDK (sign-submit.ts) + Backend
Status: RESOLVED — SDK independently derives expected commitment from user-provided parameters and verifies it appears in all_commitments_out before signing.

Fix verified in current snapshot:

  • withdraw.ts:51-54: Derives expectedCommitment = Poseidon(recipientNpK, tokenFr, amountFr) from recipientEvmAddress + token + amount
  • sign-submit.ts:90-109: Verifies server's all_commitments_out matches expected commitment BEFORE signing
  • Error message explicitly warns: "This may indicate a malicious backend."
  • Test coverage: withdraw.test.ts has explicit test for "throws on output commitment mismatch (malicious backend)"

Status: FIXED.


BK-02 (HIGH): Blind Transfer Signing — ✅ RESOLVED

Severity: HIGH
Component: SDK + Backend
Status: RESOLVED — SDK independently derives expected output commitments from user-supplied transfer instructions and verifies all of them against all_commitments_out before signing.

Fix verified in current snapshot:

  • transfer.ts:49-54: Maps each transfer instruction to derive expected commitments via Poseidon(npk, token, amount)
  • sign-submit.ts:90-109: Same verification logic applies before signing
  • Test coverage: transfer.test.ts has tests for "throws on output commitment mismatch (malicious backend)", "throws on extra commitment from malicious backend", and "rejects tampered message_hash"

Status: FIXED.


HIGH-01 (HIGH): Nullifier Pairwise Uniqueness Missing

Severity: HIGH
Component: ZK Circuit (spend.circom)
Status: Open — Requires circuit fix before mainnet

Root Cause: The Spend circuit computes per-slot nullifiers and validates each against nullifiers[i], but imposes no pairwise distinctness constraint. Two or more input slots can use the same note index and produce identical nullifiers.

Code: spend.circom:115: (1 - nullifierIsZero[i].out) * (nullHash[i].out - nullifiers[i]) === 0 — validates per-slot only.

Attack: Same note in two slots with valueIn[0] + valueIn[1] = valueIn[0] passes balance conservation. Both slots pass Merkle proof. On-chain _spend(N) called twice — second call succeeds because setting the same nullifier key to true is a no-op. Result: one note double-spent.

Required Remediation: Add pairwise-inequality constraint across all nIn nullifiers.


HIGH-02 (HIGH): Nullifier Pre-flight Only Checks First Nullifier

Severity: HIGH
Component: Relayer (broadcast.rs:168)
Status: Open — Requires fix before mainnet

Root Cause: broadcast.rs:168 checks only nullifiers[0]. For a 10-input transfer, 9 of 10 nullifiers are never verified before broadcast.

Required Remediation: Check ALL nullifiers before broadcast.


API-01 (HIGH): Deposit Prepare Missing Authorization

Severity: HIGH
Component: Backend (handlers.rs:67-77)
Status: Open — Requires ownership assertion before mainnet

Root Cause: post_prepare_deposit calls get_user(tenant_id, project_id, unlink_address) which validates the user exists in the tenant/project, but does NOT assert that the API key owner owns the address.

Required Remediation: Assert user.address == req.unlink_address after the lookup.


FE-01 (HIGH): resolveProjectBySlug Missing Tenant Check

Severity: HIGH
Component: Frontend (resolvers.ts:51-55)
Status: Open — Requires tenant filter before mainnet

Root Cause: projects.find((p) => p.slug === slug) has no tenant ownership filter.

Required Remediation: Filter by tenant ownership: projects.find((p) => p.slug === slug && p.tenantSlug === org.slug).


BK-03 (Medium): Prepared Transaction Quota Pre-Check Race

Severity: Medium
Component: Backend (transaction.rs, transaction_store.rs)
Status: Open — quota bypass, no fund loss

Root Cause: prepare_common runs count_active_prepared (SELECT COUNT without lock) pre-checks before the atomic create_prepared_atomic commit. Concurrent requests can both pass pre-checks while racing on the same note leaves.

Impact: Per-user/per-tenant quota temporarily exceeded under sustained concurrent load. Note reservation integrity is maintained by FOR UPDATE row locks on note rows — double-spend is blocked. This is a quota bypass, not a fund-loss path.

Required Remediation: Move concurrent prepare count into the atomic transaction with SELECT COUNT(*) ... FOR UPDATE.


BK-04 (Medium): Full Spend Witnesses Persisted in Postgres

Severity: Medium
Component: Backend (transaction.rs, transaction_store.rs)
Status: Open — TTL-bounded window

Root Cause: TransactionPayloadCommon includes nullifying_key, random_in, and leaf_index in prepared_payload JSONB. These fields uniquely determine spending nullifiers (nullifier = Poseidon(nk, li, random_in)). Persisted at prepare time; cleared on submit or TTL expiry.

Exposure Window: Bounded by prepare TTL (45s default) + sweeper interval (15s). Database compromise (SQL injection, insider, backup leak) exposes witness material enabling nullifier derivation for prepared-but-not-submitted transactions.

Required Remediation: Remove sensitive witness material from prepared_payload. Pass to prover via ephemeral channel at prove-time.


BK-05 (Medium): Burner Service Has No Per-Tenant Rate Limit

Severity: Medium
Component: Backend (burner.rs, config.rs:352–356)
Status: Confirmed

Root Cause: BurnerService uses global gas_funding_wei (default 0.0005 ETH/burner) with no per-tenant cap, per-project budget, or rate limit on burner creation.

Attack Scenario: Malicious tenant scripts burner creation → 1,000 burners/hour = 0.5 ETH/hour drain from relayer hot wallet.

Required Remediation: Per-tenant max_burners_per_hour cap, per-project burner_gas_budget_wei cap, API-level rate limiting, relayer hot wallet balance monitoring.


III. Contract Layer Verification

BK-01/BK-02 Fix Verification (SDK Output Commitment Derivation)

Both findings are now RESOLVED. The SDK independently derives expected output commitments from user-provided parameters and verifies them against all_commitments_out before signing. The verification happens BEFORE the eddsaSign() call, ensuring the SDK never signs a transaction with a manipulated recipient or amount.

Field Bounds Parity (L-1 Remediated)

All insertion/spend paths consistently enforce < SNARK_SCALAR_FIELD:

Path Check Location
_validateNote npk < SNARK_SCALAR_FIELD UnlinkPool.sol:473
_spend nullifierHash < SNARK_SCALAR_FIELD State.sol:103
InternalLazyIMT._insert leaf < SNARK_SCALAR_FIELD LazyIMT.sol:110
InternalLazyIMT._update leaf < SNARK_SCALAR_FIELD LazyIMT.sol:129
_buildPublicSignals Always in-field (>> 3 truncation) UnlinkPool.sol:590
_computeContextHash % SNARK_SCALAR_FIELD reduction UnlinkPool.sol:494

ERC-7201 Storage Isolation

Three distinct namespaces with non-overlapping slots:

  • unlink.storage.State: 0xd7df6c02... (merkleRoot, nullifiers, verifierRouter)
  • unlink.storage.UnlinkPoolRelayers: 0xd8b60772... (relayer registry)
  • unlink.storage.UnlinkPool: 0x06e81dc1... (pool config)

All verified via Upgradeable.t.sol:125–139 test vectors.

BN254 Precompile Probes

Three-probe strategy with cross-validation:

  • _probePairing: e(g1,g2)·e(-g1,g2)=1 AND e(g1,g2) alone≠1
  • _probeG1Mul: g1·2 == 2·g1 (KAT) AND g1·3 ≠ g1·2 (input sensitivity)
  • _probeG1Add: g1+g1 == 2·g1 (KAT)
  • Cross-validation: g1·3 (via G1Mul) must equal g1+2·g1 (via G1Add)

A precompile returning input-independent constants cannot pass both KATs AND the cross-equality check.

Withdrawal Slot Binding

// UnlinkPool.sol:631–636
uint256 wSlot = uint256(c.outputCount) - 1;
if (_txn.newCommitments[wSlot] == 0) revert PoolWithdrawalSlotZero();
if (_txn.newCommitments[wSlot] != hashNote(_txn.withdrawal)) revert PoolInvalidWithdrawalCommitment();

Withdrawal must be at wSlot = 3 (for shape (10,4)), must be non-zero, and must equal Poseidon(npk, token, amount) computed from the withdrawal struct fields.

Cross-Instance/Cross-Chain Replay

// UnlinkPool.sol:491–494
return uint256(keccak256(abi.encode(
    block.chainid, address(this), keccak256(abi.encode(_ciphertexts))
))) % Constants.SNARK_SCALAR_FIELD;

Binds to chainid (EIP-1344) and address(this). Cross-instance and cross-chain replay blocked by CrossInstanceReplay.t.sol.


IV. ZK Tooling Verification

Path Traversal Prevention (download-artifacts.sh)

  • Allowlist: Circuit names validated against circuits.json keys
  • Canonicalization: realpath -m resolves ../ before writing
  • Bounds check: Destination path must start with $ARTIFACTS_DIR
  • Filename validation: Filenames validated via $CIRCUIT_NAME_REGEX before checksum lookup

Version Binding (upload-artifacts.sh)

Version computed from: circuit source + all artifact checksums + verifier VK hash + git SHA. The version tag binds the complete uploaded bundle, not just source. Any artifact drift invalidates the version.

Drift Checking (check-zk-artifacts.sh)

  • Downloads manifest and checksums
  • Rebuilds circuit from source (circomkit compile)
  • Compares source-built WASM and witness_calculator against downloaded checksums
  • Runs snarkjs zkey verify on downloaded zkey against source-built R1CS
  • Fails if any checksum mismatch (exit 1) — no silent bypass

Hardener Idempotency (harden_verifier.py)

  • Checks ALL 4 hardening markers before accepting a verifier
  • Aborts if any marker is missing or partial
  • Unique single-line anchors prevent silent anchor drift

V. Backend Deep Dive

EdDSA-Poseidon Crypto (eddsa.rs, poseidon.rs, nullifier.rs)

All crypto primitives verified against test vectors:

Primitive Check Status
verify_poseidon Public key on curve
R8 point on curve
Public key in subgroup (cofactor 8)
R8 in subgroup (cofactor 8)
Identity point rejection
Signature malleability (s < SUBORDER)
multi_stage_message_hash Matches V2 circuit construction
sha256_public_signals_hash top253(sha256(packed)) — matches Solidity
derive_nullifier Poseidon(nk, li) — matches circuit constraint 3
derive_commitment Poseidon(npk, token, amount) — matches contract hashNote

Transaction Service (transaction.rs)

  • Prepare flow: Note snapshot → selection → commitment derivation → create_prepared_atomic with FOR UPDATE locks ✅
  • Submit flow: Signature verification → prover dispatch → claim_prepared_atomic
  • Commitment construction: Consistent with UnlinkPool.hashNote
  • Field element handling: FieldElement enforces decimal string format, rejects hex ✅

Prover (prover/src/local.rs)

  • Concurrency: Semaphore limits concurrent provers (acquire/release on all paths) ✅
  • Timeout: Split budget (30% witness / 70% rapidsnark), process killed on timeout ✅
  • Normalization: normalize_proof validates sentinels, swaps pi_b pairs for Solidity ✅
  • Cleanup: tempfile::TempDir ensures tmpdir cleanup on all exit paths ✅

Relay Worker (workers/src/relay/)

  • Permit2 deadline pre-flight: 30s margin with pre-epoch clock fallback (u64::MAX) ✅
  • Nullifier pre-flight: DB check (mitigated by on-chain _spend revert) ✅
  • Hash persistence: Written before watcher spawn (crash recovery) ✅
  • Simulation fail-open: Transport errors proceed to broadcast; revert data caught ✅
  • UE-427 fix: send_error_may_have_landed only surfaces hash for genuine transport failures ✅

Ingester (ingester/src/handler.rs)

  • Event processing: Merkle root verified against on-chain event ✅
  • Replay idempotency: Gap detection, partial overlap detection ✅
  • Tree write lock: Held until DB writes complete ✅
  • Schema drift: Returns Ok(()) with warning log (ING-01 informational) ✅

Auth Cache (cached_verifier.rs)

  • Eviction on Unauthorized: Foreground path (line 274–276) AND background SWR path ✅
  • Stale-while-revalidate: Entries served within ttl + max_stale on upstream Unavailable ✅
  • No resurrection: Eviction on Unauthorized prevents revoked key stale-serve ✅
  • Key hashing: SHA-256 hashed keys stored, never raw keys ✅

VI. Out-of-Scope (Confirmed)

These areas were explicitly excluded and are documented for future audit rounds:

Item Reason
Formal proofs (G1–G5) Separate formal audit engagement required
Phase-2 ceremony integrity Out of scope per SECURITY.md
Frontend dashboard UI logic Out of scope per SECURITY.md
Generated verifier contracts Excluded per AGENTS.md; hardened via harden_verifier.py
Multi-tenancy timing attack isolation Not explicitly mitigated
Ingester RPC source diversity Single Goldsky RPC source
Relayer key management EOAs loaded from env vars
Backend secret storage Env var loading, no Vault
ZK proving infrastructure (rapidsnark path) Not exercised in current CI

VII. Pre-Mainnet Remediation Priority

Critical (Must Fix Before Mainnet)

ID Severity Component Status Action Required
HIGH-01 HIGH ZK Circuit ⚠️ Open Add pairwise-inequality constraint across all nIn nullifiers
HIGH-02 HIGH Relayer ⚠️ Open Check ALL nullifiers in broadcast pre-flight
API-01 HIGH Backend ⚠️ Open Assert unlink_address ownership
FE-01 HIGH Frontend ⚠️ Open Filter project lookup by tenant ownership

High (Fix Before Mainnet)

ID Severity Component Status Action Required
DEPS-03 MED ZK tooling ⚠️ Open Add overrides: { "snarkjs": "^0.7.6" } to package.json
FE-02 MED Frontend ⚠️ Open Add rate limiting to server actions
BK-03 MED Backend ⚠️ Open Move count checks into atomic transaction
BK-05 MED Backend ⚠️ Open Add per-tenant burner rate limits
DEPS-04 MED ZK/SDK ⚠️ Open Update vite/vitest in dev deps

Medium (Post-Mainnet)

ID Severity Component Status Action Required
BK-04 MED Backend ⚠️ Open Remove sensitive witness fields from prepared_payload
ING-01 LOW Backend ℹ️ Informational Add schema drift metric counter

Resolved (No Action Required)

ID Severity Component Status
BK-01 HIGH SDK withdrawal RESOLVED
BK-02 HIGH SDK transfer RESOLVED
RL-02 HIGH Relayer burner verification RESOLVED
DEPS-01 HIGH Next.js upgrade RESOLVED
DEPS-02 HIGH @clerk/nextjs upgrade RESOLVED

VIII. Risk Summary

Component Risk Level Basis
UnlinkPool (deposit/transfer/withdraw) Low Well-gated, reentrancy-guarded, field-bounded, 12 invariants
VerifierRouter (circuit registry) Low Owner-gated, shape-immutable, 1-to-1 binding
State (merkle tree + nullifiers) Low Field bounds on all insertions, monotonic storage
Generated Verifier (spend_10x4_v1) Low Hardened (returndata + field range), adversarial-tested
Poseidon (T3/T4) Low Vendored assembly, not modified
InternalLazyIMT Low Vendored, field bounds enforced
Backend (transaction service) Medium HIGH-01, HIGH-02, API-01, BK-03–05 require fixes
SDK (sign-submit) Low ✅ BK-01, BK-02 resolved — independent verification added
Relayer (broadcast + recovery) Medium ⚠️ HIGH-02 open; RL-02 resolved
Ingester (event processing) Low Replay-idempotent, merkle-verified
Frontend dashboard Medium ✅ DEPS-01, DEPS-02 resolved; FE-01, FE-02 open
ZK circuit (spend.circom) Medium ⚠️ HIGH-01 open — nullifier pairwise uniqueness needed
ZK tooling Medium ⚠️ DEPS-03 open — snarkjs transitive override needed

Generated by LFG Labs / Verity audit mission — final consolidated report — 2026-05-13 Mission: d29546d5-0d67-45be-a07a-7d7a1c3dd1be

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment