Veilon Info
A zk-native cross-chain wallet and protocol for private, untraceable transfers, swaps, and bridging powered by stealth addresses & encrypted transaction layers.
TrustNet Score
The TrustNet Score evaluates crypto projects based on audit results, security, KYC verification, and social media presence. This score offers a quick, transparent view of a project's credibility, helping users make informed decisions in the Web3 space.
Real-Time Threat Detection
Real-time threat detection, powered by Cyvers.io,
is currently not
activated
for this project.
This advanced feature provides continuous monitoring and instant alerts to safeguard your assets from potential security threats. Real-time detection enhances your project's security by proactively identifying and mitigating risks.
For more information, click here.
Security Assessments
Summary and Final Words
No crucial issues found
The contract does not contain issues of high or medium criticality. This means that no known vulnerabilities were found in the source code.
Contract owner cannot mint
It is not possible to mint new tokens.
Contract owner cannot blacklist addresses.
It is not possible to lock user funds by blacklisting addresses.
Contract owner cannot set high fees
The fees, if applicable, can be a maximum of 25% or lower. The contract can therefore not be locked. Please take a look in the comment section for more details.
Token transfer can be locked
Owner can lock user funds with owner functions.
Token cannot be burned
There is no burning within the contract without any allowances
Ownership is not renounced
The owner retains significant control, which could potentially be used to modify key contract parameters.
Contract is not upgradeable
The contract does not use proxy patterns or other mechanisms to allow future upgrades. Its behavior is locked in its current state.
Scope of Work
This audit encompasses the evaluation of the files listed below, each verified with a SHA-1 Hash. The team referenced above has provided the necessary files for assessment.
The auditing process consists of the following systematic steps:
- Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
- Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
- Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
- Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
- Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
- Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
- Actionable Recommendations: Provide detailed, specific, and actionable steps to secure and optimize the smart contracts.
A file with a different Hash has been intentionally or otherwise modified after the security review. A different Hash may indicate a changed condition or potential vulnerability that was not within the scope of this review.
Final Words
The following provides a concise summary of the audit report, accompanied by insightful comments from the auditor. This overview captures the key findings and observations, offering valuable context and clarity.
Smart Contract Analysis Statement
Contract Analysis
The Veilon protocol implements a ZK-proof-based privacy pool system across four core smart contracts (PrivacyPoolV2, Verifier, EmergencyExit, Governance) with supporting Groth16 verifier contracts, a backend relayer service, and a React Native mobile wallet. While the architectural intent follows established privacy pool patterns (commitment-based shielding, nullifier-based spending, ZK proof verification), the implementation contains fundamental design flaws that prevent the protocol from delivering its stated privacy guarantees:
- Broken Privacy Model: The entire ZK privacy system is structurally non-functional. There is no Merkle tree implementation — commitments are stored in flat mappings and directly referenced in calldata, making all transaction flows fully traceable on-chain. The anonymous set that is essential to any privacy protocol does not exist.
- Server-Side Proof Generation: ZK proofs are generated on the backend relayer, which receives all private values (secret, nullifier, randomness) in plaintext from the mobile app. The relayer has complete knowledge of every commitment and can link all deposits to withdrawals. This negates the entire purpose of zero-knowledge proofs — the proving party already knows all secrets.
- Mock Proofs in Production Path: The compiled ZK circuit artifacts (.wasm, .zkey) are missing from the repository. All three circuits (shield, unshield, transfer) fall back to generating random mock proofs with no production guard. The verification method silently returns
truewhen the verification key is absent. - EmergencyExit is Non-Functional: The entire EmergencyExit contract (502 lines) does not perform any actual fund movement. The triggerPanicButton and redistribution flow only write structs to storage — no ETH or token transfers occur. The recovery function contains an unimplemented TODO comment. Additionally, private keys are stored on-chain in plaintext, completely defeating stealth address privacy.
- Proof/Public Input Binding Failure: The smart contract verifies ZK proofs but does not enforce that calldata arguments (commitments, amounts, recipients, tokens) match the public inputs consumed by the circuit. This breaks the fundamental statement-to-state binding that ZK proofs are meant to provide.
- Cross-Component Secret Exposure: Commitment secrets are logged to console on both the backend and mobile app, stored unencrypted in AsyncStorage on the mobile device, returned in HTTP API responses, and transmitted without certificate pinning — creating multiple attack surfaces across the entire stack.
Ownership Privileges
Ownership across all contracts follows a single-owner model via OpenZeppelin's Ownable. No multi-sig, timelock, or governance gate is required for any privileged action. The owner retains the following privileges:
- Emergency Fund Drain: The owner can call
emergencyWithdraw(token, amount)on PrivacyPoolV2 to withdraw any amount of any token or ETH from the pool — instantly, with no timelock, no event emission, and no multi-sig requirement. This is a direct rug-pull vector. - Pause All Operations: The owner can pause and unpause PrivacyPoolV2, EmergencyExit, and Governance at any time, blocking all shield, unshield, transfer, and governance operations.
- Token Whitelisting/De-whitelisting: The owner can enable or disable tokens for the privacy pool via
setTokenWhitelist(). De-whitelisting a token while user funds are shielded could trap those funds. - Circuit Control: The owner of the Verifier contract can register and deregister ZK verification circuits via
updateCircuit(), potentially disabling proof verification entirely. - Fee Control (No Cap): The owner can set the emergency fee to any value via
updateEmergencyFee()with no maximum cap enforced. - Governance Control: The owner controls voter registration and deregistration, and can execute approved proposals. Proposal execution is owner-gated, not automatically triggered by vote results.
- Relayer Control: The owner of EmergencyExit can add and remove authorized relayers without event emission.
- Limitation: The owner cannot upgrade contracts — no proxy or upgradeability pattern is implemented.
- Limitation: The owner cannot blacklist individual user addresses — only tokens can be whitelisted/de-whitelisted.
- Limitation: The owner cannot mint or burn tokens — the protocol handles existing ERC20 tokens and native ETH only.
Security Features
Despite the critical architectural issues, the contracts implement several positive security measures:
- Reentrancy Protection: All state-changing functions use OpenZeppelin's ReentrancyGuard across PrivacyPoolV2, EmergencyExit, and Governance.
- Pausable Emergency Stop: All three main contracts implement the Pausable pattern, allowing operations to be halted in case of a detected exploit.
- Nullifier Tracking: Spent nullifiers are recorded on-chain (
spentNullifiersmapping) to prevent double-spending of commitments. - SafeERC20 Usage: ERC20 interactions in PrivacyPoolV2 use OpenZeppelin's SafeERC20 library, protecting against non-standard token implementations.
- Groth16 Verification Architecture: The Verifier contract correctly delegates to separate snarkJS-generated Groth16 verifier contracts per circuit type (shield, unshield, transfer), following the standard ZK verification pattern.
- Helmet Security Headers: The backend relayer applies Helmet middleware for HTTP security headers.
- SecureStore for Private Keys: The mobile app correctly stores private keys and mnemonics in the device's hardware-backed SecureStore (though commitment secrets are not afforded the same protection).
Auditor's Note
This audit report encompasses a full-stack security analysis of the Veilon privacy pool protocol, covering smart contracts (Solidity), the backend relayer (TypeScript/Express), and the mobile wallet (React Native/Expo). The analysis was conducted as a static, read-only audit — no code changes were applied and no tests were executed. Findings were cross-referenced across all three components and subjected to peer review with severity adjustments documented per finding.
The protocol, in its current state, does not deliver functional privacy guarantees. The absence of a Merkle tree, server-side proof generation with full secret exposure, missing circuit artifacts, and incomplete proof-to-state binding mean the ZK layer serves as a structural placeholder rather than a working privacy mechanism. The EmergencyExit subsystem (the largest contract at 502 lines) is entirely non-functional for its stated purpose. We recommend the Veilon team treat the 13 critical findings as blocking issues that must be resolved before any mainnet deployment or handling of real user funds.
This analysis did not include economic analysis of the protocol's tokenomics, nor did it cover off-chain infrastructure security (hosting, CI/CD, key management). The ZK circuits themselves (.circom source files) were not in scope as they were not present in the repository. We recommend investors and users conduct their own research and exercise caution until all critical findings have been addressed and independently verified.
Files and details
Findings and Audit result
critical Issues | 7 findings
Resolved
#1 critical Issue
Fundamental Design Flaw
The triggerPanicButton function (line 185-230) and _executeRedistribution (line 235-270) NEVER actually transfer any ETH or tokens. The redistribution only creates StealthWallet structs in storage via _generateStealthWallet, which computes deterministic hashes. No interaction with PrivacyPoolV2 occurs, no transfer()/call()/safeTransfer() is invoked. The entire emergency exit flow is a no-op for actual fund protection. The contract must be redesigned to perform actual fund movement.
Resolved
#2 critical Issue
Privacy Violation
The _generateStealthWallet function (line 275-303) generates viewingKey and spendingKey and stores them in the public sessionStealthWallets mapping. The getSessionStealthWallets function (line 438-444) returns all wallet data including these keys to any caller. In a stealth address scheme, viewing and spending keys must remain private. Storing them on-chain in plaintext completely defeats the purpose of privacy. Never store private key material on-chain; generate off-chain and submit only commitments.
Resolved
#3 critical Issue
Incomplete Implementation
The recoverStealthWallet function (line 375-404) validates recovery proof and marks wallet as recovered, but actual fund transfer is NOT implemented. Line 402 contains: '// Transfer funds back to user (implement actual transfer logic)'. Even if a user provides a valid recovery proof, they receive nothing. This is an incomplete implementation deployed to testnet. Implement actual fund transfer logic or remove the feature until complete.
Resolved
#4 critical Issue
Missing Proof Verification
The triggerPanicButton function accepts bytes calldata _proof (line 186) as a 'zero-knowledge proof of fund ownership' but this proof is NEVER verified. It is merely hashed and stored in panicProofs[sessionId]. The Verifier contract is referenced but never called. Anyone with emergency config set can trigger the panic button for any arbitrary _totalAmount without proving fund ownership. The proof must be verified against the Verifier contract before any redistribution.
Resolved
#5 critical Issue
Unbounded Array Growth / DoS
The configureEmergencyExit function (line 155-177) uses push() to add emergency contacts but never clears the existing array. Each call appends to config.emergencyContacts. A user calling this multiple times to update config accumulates duplicate contacts, causing unbounded storage array growth, increasing gas costs for _notifyEmergencyContacts which iterates all contacts, and potential DoS if the array exceeds block gas limits. Add 'delete config.emergencyContacts;' before the loop and add a max contacts limit.
Resolved
#6 critical Issue
Unreachable Code
The cancelEmergencySession function (line 411-433) requires session.status == 1 (processing). However, triggerPanicButton calls _executeRedistribution synchronously, which immediately sets session.status = 2 (completed) at line 262 before the function returns. By the time triggerPanicButton finishes, the session is already completed and can never be cancelled. The EMERGENCY_TIMEOUT window is meaningless. If cancellation is required, the redistribution should be a two-phase process with a delay between initiation and execution.
Resolved
#7 critical Issue
Proof/Public Input Binding Failure
In unshield() and privateTransfer(), the contract verifies proof.publicInputs but does not enforce that calldata arguments (inputCommitment, outputCommitment/changeCommitment, outputAmount, changeAmount, token, recipient) equal the corresponding public inputs consumed by the circuit. This breaks statement-to-state binding and can allow state transitions against mismatched on-chain commitments. Add explicit equality checks between calldata fields and public input indices before any state transition.
high Issues | 1 findings
Resolved
#1 high Issue
Centralization / Rug Pull
The emergencyWithdraw function (line 344-355) allows the owner to withdraw any amount of any token or ETH with no restrictions, no time-lock, no multi-sig requirement, and no event emission. In a trust-minimized deployment this is a rug-pull vector where the owner can drain the pool instantly. Add event emission, implement a time-lock mechanism (e.g., 48-hour delay), and require multi-sig or governance authorization.
medium Issues | 10 findings
Pending
#1 medium Issue
Gas Waste / Design Flaw
Functions verifyShieldProof, verifyUnshieldProof, and verifyTransferProof (lines 128-157) call this.verifyProof(...) which forces an external call to the contract itself. This wastes ~2600 gas per invocation for the CALL opcode, forces external instead of view visibility, introduces unnecessary call stack complexity, and the nonReentrant modifier on verifyProof means these wrappers acquire the reentrancy lock unnecessarily. Extract core logic into an internal function.
Pending
#2 medium Issue
Checks-Effects-Interactions Hardening
In unshield() (line 179-241), ETH is sent to recipient (line 225) before creating the optional change commitment (line 234) and updating totalUnshieldedByToken (line 238). nonReentrant reduces exploitability, but ordering still violates CEI best practices and increases upgrade or integration risk. Move all state effects before external interactions.
Pending
#3 medium Issue
Cross-Chain Replay
ZK proof verification does not include block.chainid or any chain-specific identifier in the public inputs. The contracts are deployed on multiple chains (Sepolia, Arbitrum, BSC, Mumbai). A valid proof generated and used on one chain could be replayed on another chain where the same commitment exists. Include block.chainid as a public input to ZK circuits or include contract address in nullifier derivation.
Pending
#4 medium Issue
Missing Input Validation
The PrivacyPoolV2 constructor (line 72-77) accepts _verifierContract without validating it is not address(0). If deployed with a zero address, all unshield and privateTransfer calls would revert with opaque errors when calling the verifier. The Verifier.sol constructor already has this check — maintain consistency. Also declare verifierContract as immutable.
Pending
#5 medium Issue
Predictable Randomness
The 'random' distribution strategy (type 2) in _calculateDistribution (line 330-339) uses keccak256(abi.encodePacked(i, _totalAmount)) as randomness. Both i and _totalAmount are known by the caller, making the distribution entirely deterministic and predictable. The last wallet receives all remaining funds which could be disproportionate. Also missing validation that maxAmount > minAmount to prevent underflow in the modulo operation.
Pending
#6 medium Issue
Missing Input Validation
The weighted distribution strategy (type 1) at line 323-329 copies the user-supplied amounts array directly without validating that the sum of amounts equals _totalAmount. A user could specify amounts that sum to more or less than the total, leading to inconsistent accounting. Add validation: compute the sum of _strategy.amounts and require it equals _totalAmount.
Pending
#7 medium Issue
Architecture / Design Pattern
The Verifier contract relies on string-based dispatch ('shield', 'unshield') for routing verification requests. This approach is gas-inefficient (requires string hashing/comparison) and fragile. A more robust architecture would use 4-byte function selectors, enums, or separate entry points to enforce type safety and reduce gas costs, avoiding the risks associated with string manipulation.
Pending
#8 medium Issue
Hash Collision / Cache Key Integrity
verifyProof() computes proofHash with abi.encodePacked(circuitType, ...) where circuitType is dynamic. This can produce hash collisions across crafted packed inputs and poison proof cache accounting, even if it does not directly bypass Groth16 validity checks. Replace abi.encodePacked with abi.encode for collision-safe canonical encoding.
Resolved
#9 medium Issue
Weak Randomness
The _generateStealthWallet function (line 275-303) uses block.timestamp, block.prevrandao, and the contract nonce as entropy sources for stealth wallet key generation. All are predictable: block.timestamp is known, block.prevrandao is influenced by validators (post-merge), and nonce is a simple storage counter. A validator/proposer can predict or manipulate the generated stealth wallet addresses. Use Chainlink VRF or generate wallet parameters off-chain.
Pending
#10 medium Issue
Deprecated Pattern
The withdrawFees function (line 499-501) uses payable(owner()).transfer(address(this).balance) which forwards only 2300 gas. If the owner is a multi-sig wallet or any contract (e.g., Gnosis Safe), this will fail because 2300 gas is insufficient for most contract receive functions. Replace with: (bool success, ) = owner().call{value: address(this).balance}(''); require(success, 'Fee withdrawal failed');
low Issues | 9 findings
Pending
#1 low Issue
Dead Code
TransferVerifier_NEW.sol contains a contract named Groth16Verifier with only 5 public inputs, whereas the actual TransferGroth16Verifier in TransferVerifier.sol requires 10 inputs. This file is not referenced anywhere in the codebase and appears to be leftover from development. Dead code increases attack surface and causes confusion during audits. Remove this file from the contracts directory.
Pending
#2 low Issue
Missing Events
Several functions modify critical state variables without emitting events: updateEmergencyFee() (line 460), addAuthorizedRelayer()/removeAuthorizedRelayer() (lines 467-476), configureEmergencyExit() (line 155), cancelEmergencySession() (line 411). Similarly in PrivacyPoolV2.emergencyWithdraw() (line 344) and Verifier.updateCircuit() (line 191). Emit events for all state-changing functions to enable off-chain monitoring and incident response.
Pending
#3 low Issue
Gas Optimization / Immutability
State variables only set in constructors are not declared immutable: PrivacyPoolV2.verifierContract (line 66), EmergencyExit.privacyPool (line 105), EmergencyExit.verifier (line 106), Verifier.shieldVerifier (line 45), Verifier.unshieldVerifier (line 46), Verifier.transferVerifier (line 47). Declaring these as immutable saves ~2100 gas per SLOAD and enforces they cannot be changed after deployment.
Pending
#4 low Issue
Modifier Ordering
In multiple functions across PrivacyPoolV2, EmergencyExit, and Governance, the nonReentrant modifier is not the first modifier in the chain. For example, shieldETH applies whenNotPaused before nonReentrant. If an earlier modifier had a reentrancy vulnerability, the guard would not protect against it. Always place nonReentrant as the first modifier.
Pending
#5 low Issue
Unused State Variable
The Counters.Counter private _transactionId (line 51) is incremented in shieldETH, shieldERC20, and _createCommitment but is never read externally. There is no getter function and the value serves no on-chain purpose. Either add a getTransactionCount() view function or remove it to save gas on shield operations.
Pending
#6 low Issue
Logic Error
The Voter struct has a hasVoted field (line 51) but it is never set to true after a voter votes. The getVoter function always returns false for this field. Voting state is tracked per-proposal via Receipt.hasVoted, making the voter-level hasVoted field misleading. Either remove the unused field or update it in the vote() function.
Pending
#7 low Issue
Logic / Semantics
The proposalPassed function (line 309-325) returns false once a proposal is executed. This is semantically confusing for integrators because an executed proposal has already passed. Consider returning pass status independent of execution state, or rename to indicate pending execution state.
Pending
#8 low Issue
Unspecific Pragma
All contracts use ^0.8.19 which allows compilation with any 0.8.x version >= 0.8.19. Different compiler versions may produce different bytecode and have different bugs. Lock the pragma to 'pragma solidity 0.8.19;' for production deployments to ensure deterministic compilation.
Pending
#9 low Issue
Access Control
MockERC20.mint() and burn() have no access control. While this is a test contract, it is included in the contracts directory alongside production code. If accidentally deployed to mainnet, anyone could mint unlimited tokens. Move to a contracts/test/ subdirectory or add onlyOwner modifier. Ensure deployment scripts never deploy it on mainnet.
optimization Issues | 4 findings
Pending
#1 optimization Issue
Gas Optimization
In _verifyProofByType (line 263-308), keccak256(abi.encodePacked(circuitType)) is computed up to three times for string comparison against 'shield', 'unshield', and 'transfer'. Each keccak256 computation costs gas. Pre-compute these hashes as immutable bytes32 constants and compare directly to save ~200 gas per additional comparison.
Pending
#2 optimization Issue
Gas Optimization
In configureEmergencyExit (line 171), the loop uses config.emergencyContacts.push() which involves SSTORE operations inside the loop. Cache the length in a local variable and consider batch storage patterns. Additionally, _contacts.length is read from calldata on every iteration; cache it in a local variable.
Pending
#3 optimization Issue
Gas Optimization
The _transactionId counter (Counters.Counter) is incremented on every shield and _createCommitment call but never read externally. Each increment costs an SSTORE (~5000 gas for non-zero to non-zero). Remove this counter if not needed, or replace with a simple uint256 increment which is cheaper than using the Counters library.
Pending
#4 optimization Issue
Redundant Storage
Commitment data is stored redundantly: both in the shieldedTransactions mapping (which stores token, amount, etc.) AND separately in commitmentAmount and commitmentToken mappings. This doubles SSTORE costs on every shield operation. Consider removing the separate mappings and reading from shieldedTransactions directly, or vice versa.
informational Issues | 6 findings
Pending
#1 informational Issue
Broken Test
Test 'Should track total shielded correctly' calls privacyPool.totalShielded() which does not exist. The contract uses the mapping totalShieldedByToken(token) instead. This test was written for an older contract version and was never updated. Fix the test to call totalShieldedByToken with the appropriate token address.
Pending
#2 informational Issue
Broken Test
Test 'Should allow user to unshield ETH' calls unshield with old 5-parameter signature (nullifier, recipient, amount, token, proof), but the current contract expects 7 parameters (inputCommitment, changeCommitment, recipient, outputAmount, changeAmount, token, proof). The test is outdated and does not test the current contract interface.
Pending
#3 informational Issue
External Dependency
FullFlow.test.js and PrivateTransfer.test.js depend on an external relayer service running at localhost:3004 which is not included in the repository. These integration tests cannot be executed in isolation. Consider adding mock relayer responses or documenting the relayer setup requirements.
Pending
#4 informational Issue
Missing Test Coverage
Critical code paths have zero test coverage: partial unshield with change commitment, private transfer with change, emergencyWithdraw, all Governance flows (voting, execution, quorum), EmergencyExit recovery and cancellation, access control violation attempts, token de-whitelisting while funds are shielded, and reentrancy attack simulations. Achieve >95% line coverage before mainnet deployment.
Pending
#5 informational Issue
Multiple Solidity Versions
Four different Solidity version constraints are used across the project: ^0.8.19 (custom contracts), ^0.8.0 and ^0.8.1 (OpenZeppelin), and >=0.7.0 <0.9.0 (snarkJS verifiers). The snarkJS verifiers use a very wide pragma range. While this is auto-generated code, consider pinning to a specific version for consistency and deterministic compilation.
Pending
#6 informational Issue
Centralization Risk
High centralization risk across all contracts: PrivacyPoolV2 owner can pause, whitelist tokens, and drain funds; Governance owner controls voter registration/deregistration and proposal execution; EmergencyExit owner controls fees, relayers, and pause. Verifier owner can register/deregister circuits, potentially disabling proof verification. Consider implementing Ownable2Step, timelocks, or transitioning to governance-based ownership.