Skip to content

Security: MisterDefender/Obsidian

Security

SECURITY.md

Obsidian — Security & Threat Model

⚠️ Research / educational. Testnet only. Unaudited. Do not deploy to mainnet or use with real funds. This document is an honest account of what Obsidian protects, what it does not, and the assumptions it relies on.

What Obsidian is

A fixed-denomination ERC-20 shielded vault. A deposit posts commitment = Poseidon(nullifier, secret) as a leaf in an on-chain Merkle tree. A withdrawal proves, in zero knowledge, that the commitment is some leaf under a known recent root — revealing only the root and nullifierHash = Poseidon(nullifier), never which leaf. This breaks the on-chain link between depositor and recipient.

Privacy model

Hidden

  • The link between a specific deposit and a specific withdrawal.
  • Which leaf / commitment a withdrawal corresponds to.
  • The secret and nullifier (never leave the client).

Anonymity set = the number of deposits of the same denomination that have not yet been withdrawn. It is only as large as the pool is used; a withdrawal from a near-empty pool is weakly private.

Leaked / out of scope

  • Fixed denomination and the fact that a deposit/withdrawal occurred (and when) — timing analysis remains possible.
  • The recipient address (public on withdrawal).
  • If a user self-withdraws and pays gas from a funded address, that address may link them. Use a relayer (Phase 2) to avoid this; the circuit/contract already bind relayer + fee.
  • Network-level metadata (IP, RPC provider) — not addressed on-chain.

Trust assumptions

  • Trusted setup (the big one). Groth16 needs a per-circuit trusted setup. The current key comes from a single-contribution dev ceremony (packages/circuits/run.zsh). If that contributor retained the toxic waste, they could forge proofs and drain the pool. A real multi-party ceremony (security holds if ≥1 contributor is honest) is Phase 3, to be run once the circuit is frozen. Acceptable for testnet; not for production.
  • Cryptography. Groth16 soundness on BN254, and Poseidon's collision/preimage resistance.
  • On-chain hasher. The Poseidon hasher is deployed from circomlib-generated bytecode and is immutable. A test (Poseidon equivalence) asserts it matches the circuit's Poseidon bit-for-bit; a mismatch would break root computation, not create a forgery vector.

Contract-level protections

  • Double-spend: nullifierHashes mapping; a nullifier can be spent once.
  • Proof binding: recipient, relayer, and fee are constrained inside the circuit (squared), so a relayer cannot rewrite them after proving (front-running / malleability resistance).
  • Root freshness: withdrawals must reference one of the last ROOT_HISTORY_SIZE (30) roots, so honest withdrawals survive concurrent deposits without accepting arbitrary roots.
  • Field validation: commitments and tree inputs are range-checked against the BN254 field.
  • Reentrancy: ReentrancyGuard on deposit/withdraw, checks-effects-interactions ordering (state updated before transfers), and SafeERC20. Covered by a malicious-token reentrancy test.
  • Custom errors throughout for clear, gas-efficient reverts.

Static analysis

slither . runs clean (0 findings) with two detectors excluded in slither.config.json, both reviewed and accepted:

  • calls-loop — the per-level hasher.poseidon(...) is a deliberate external call to a trusted, immutable hasher; it is the standard Merkle-tree construction and cannot be inlined without a Solidity Poseidon implementation.
  • naming-convention — leading-underscore parameters and the ZERO_VALUE constant are intentional, idiomatic style.

Known limitations / TODO

  • Single-contribution dev trusted setup (→ Phase 3 multi-party ceremony).
  • No deployed relayer yet (→ Phase 2; protocol support is in place).
  • Mock USDC token for local/testnet; not a real asset.
  • Unaudited. No formal verification.

Reporting

This is a personal research project. Open an issue for anything you find.

There aren't any published security advisories