Skip to content
Veilnet v1.0

Smart Contract Design

Contract Overview

Main Responsibilities

  • Accept deposits (ETH and ERC-20)
  • Store the current valid Merkle Root
  • Process withdrawals via valid Merkle Proofs and Admin Signatures
  • Track nullifiers to prevent replay attacks

Supported Actions

  • deposit: Publicly callable
  • withdraw: Executed via proofs
  • updateRoot: Called by the shielded sequencer

Key Concepts

  • Encrypted commitments: Data structures representing user balances
  • Merkle roots: The 32-byte hash representing the state of all commitments
  • Nullifiers / spent markers: Unique hashes generated when a commitment is spent, stored on-chain to prevent double-spending

How Nullifiers Work

Purpose

Nullifiers are used to prevent replay attacks during withdrawals. Once a withdrawal is executed, its corresponding nullifier is marked as spent on-chain.

Nullifier Generation

The nullifier is deterministically generated based on the current Merkle root and the specific leaf details:

keccak256(abi.encode(root, user, token, balance))

Preventing Double-Use

The smart contract stores spent nullifiers. Any attempt to submit a proof with an already spent nullifier will revert the transaction.

Private Deposit Flow

  1. User action: User deposits 100 USDC via the frontend
  2. Commitment creation: Frontend generates an encrypted commitment for 100 USDC
  3. Encryption step: Data is secured
  4. Contract update: The user sends a transaction to VeilNet.sol depositing the funds
  5. Merkle tree insertion: The state sequencer detects the deposit and adds the new commitment to the Merkle tree
  6. User-side note/receipt: The user's dashboard reflects the new shielded balance

Private Transfer Flow

  1. Sender input: User initiates a transfer to a recipient's shielded address
  2. Recipient shielded address: The frontend prepares the secure payload for the recipient
  3. Balance update: The sequencer validates the sender's balance and processes the transfer
  4. Nullifier/spend protection: The sender's old commitment is marked as spent internally
  5. Merkle root update: The sequencer generates new commitments for both sender (change) and recipient, updating the Merkle tree
  6. Output commitment creation: The new state is finalized

Private Withdrawal Flow

  1. User claim: User requests to withdraw 50 USDC to a specific public address
  2. Verification step: The proving layer verifies the user's balance and generates a Merkle Proof
  3. Spend/nullifier check: The proving network ensures the funds haven't been spent
  4. Output release: An admin transaction is submitted to VeilNet.sol with the Proof and an EIP-712 WithdrawAuth signature. The contract verifies the proof against the current Root, checks the nullifier, and releases the funds
  5. Privacy considerations: While the withdrawal amount and destination are public, the source of the funds (the user's internal Veilnet balance) remains hidden

Failure Cases and Protections

  • Invalid signatures: Rejected by the sequencer or contract
  • Reused commitments: Rejected; old commitments cannot be spent twice
  • Double-spend attempts: Prevented by the on-chain and stateless nullifier checks
  • Wrong Merkle root: On-chain transactions will revert if the provided proof doesn't match the stored Root
  • Incorrect encrypted payload: Cannot be decrypted or verified; transaction fails safely
  • Replay protection: Enforced via EIP-712 nonces and nullifiers

Privacy layer for EVM chains