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.
Contract cannot be locked
Owner cannot lock any user funds.
Token cannot be burned
There is no burning within the contract without any allowances
Ownership is renounced
The contract does not include owner functions that allow post-deployment modifications.
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.
No Audit Comments.
Files and details
Findings and Audit result
critical Issues | 3 findings
Acknowledged
#1 critical Issue
Catastrophically Weak PIN Hashing
The securityStore uses a custom bit-shifting hash function with a hardcoded salt ('veilon_salt_2024') to hash user PINs. This is not a cryptographic hash function — it produces a 32-bit integer, meaning there are at most ~4 billion possible outputs. With a 4-6 digit PIN space (10,000 to 1,000,000 PINs), the entire keyspace can be brute-forced in microseconds. The hardcoded salt means all users share the same salt, enabling rainbow table attacks. Critically, a proper PinSecurityService exists at services/pin-security.ts that uses SHA-256 with random per-user salts, but the securityStore does NOT use it.
Resolved
#2 critical Issue
Commitment Secrets Stored Unencrypted
All commitment data — including secret, nullifier, randomness (the private witness values for ZK proofs) — is stored in AsyncStorage via JSON.stringify. On Android, AsyncStorage uses an unencrypted SQLite database. On a rooted device or via a backup extraction, an attacker can read all commitment secrets and: (1) reconstruct all user commitments, (2) generate valid nullifier hashes to spend user funds, (3) compute the user's full private balance and transaction history. Private keys are correctly stored in SecureStore (encrypted), but the commitment secrets that protect shielded funds are not.
Pending
#3 critical Issue
Private Secrets Transmitted to Backend Relayer
The mobile app transmits commitment secrets (secret, nullifier, randomness, changeSecret, changeNullifier, changeRandomness, inputSecret, inputNullifier, etc.) to the backend relayer in plaintext via HTTP POST. The relayer sees all private values needed to reconstruct commitments, generate nullifier hashes, and forge proofs. This completely negates the privacy guarantees of ZK proofs. In a genuine privacy protocol, proof generation MUST happen client-side so that secret values never leave the user's device.
high Issues | 2 findings
Pending
#1 high Issue
Extensive Console Logging of Private Keys and Secrets
The mobile app logs private keys, commitment secrets, nullifiers, and proof data to the JavaScript console. On Android, these logs are accessible via adb logcat. On iOS, they are accessible via Xcode console. In production builds, console.log may still be active and can be captured by crash reporting tools or device logs.
Resolved
#2 high Issue
No PIN Brute-Force Protection
The verifyPin function has no rate limiting, lockout mechanism, or attempt counter. With a 4-digit PIN (10,000 possibilities) and no delays, an attacker with physical device access can brute-force the PIN in seconds. Even with the weak hash (see FE-CRIT-01), the attack is trivial.
medium Issues | 7 findings
Pending
#1 medium Issue
Proof Format Conversion — Fragile and Inconsistent
The mobile app converts snarkjs proof format to the on-chain Verifier.Proof struct in two different places with slightly different patterns. The pi_b coordinate handling is critical: snarkjs outputs pi_b in one order, but the Groth16 verifier expects reversed inner arrays. The Verifier.sol contract (line 276-279) reverses pi_b internally. The mobile app does NOT reverse pi_b before sending (which is correct since the contract does it), but a code comment says 'Fixed: DO NOT reverse - tests show this is correct format' — indicating this was arrived at through trial-and-error rather than understanding. The submitUnshieldTransaction and submitPrivateTransfer use different proof construction logic, creating inconsistency risk.
Pending
#2 medium Issue
Cryptographic Randomness Source Concerns
The mobile app generates change secrets and output secrets using ethers.randomBytes(31). In React Native, ethers.randomBytes depends on the polyfilled crypto.getRandomValues. The app imports 'react-native-get-random-values' for this, but the quality of randomness depends on the native implementation. For a privacy-critical application, the randomness source should be explicitly validated.
Pending
#3 medium Issue
No TLS Certificate Pinning
API calls to the relayer use HTTPS in production but have no certificate pinning. This makes the app vulnerable to MITM attacks via compromised or rogue CAs. Given that the relayer receives secret commitment values (see FE-CRIT-04), a MITM attacker could intercept all private transaction data.
Pending
#4 medium Issue
Dual Private Key Storage Paths
Private keys are stored in SecureStore under two different key patterns: (1) walletStore uses 'wallet_pk_{address}' and 'wallet_mnemonic_{address}' per wallet, (2) veilon-sdk's initializeFromStorage uses 'wallet_private_key' (a single global key). This creates confusion about which key is the source of truth and could lead to stale or incorrect key usage if one path is updated but not the other.
Pending
#5 medium Issue
Hardcoded Contract Address Mismatch
The syncCommitments method hardcodes the PrivacyPool address as '0x2f2B95f475F9774F52bE449e060fC52727B11dEB' for Sepolia. However, the canonical contract address in constants/contracts.ts is '0x33a5f045eECb8C17b0CBeaE29ed08e6D0E2ec5f5'. This means commitment synchronization queries the WRONG contract, so the spent status of nullifiers is never correctly synced. Users may see stale balances and attempt to double-spend commitments that were already spent, causing transaction failures.
Pending
#6 medium Issue
Cross-Chain Replay Attack — No Chain Binding in Proofs
The mobile app sends chainId and network to the backend relayer, but these are not included in the ZK circuit inputs or public signals. A proof generated on one chain can be replayed on another. The mobile app does not validate that the proof it receives back is bound to the current chain before submitting it to the smart contract.
Pending
#7 medium Issue
Raw Nullifier Used for On-Chain Query Without Format Conversion
The syncCommitmentsWithBlockchain method queries spentNullifiers(c.nullifier) using the raw nullifier string from local storage. The contract expects bytes32, and ethers.js will attempt auto-conversion. If the nullifier is stored as a decimal string (from Poseidon hash), the conversion to bytes32 may not match how the contract stored the nullifierHash. The contract stores bytes32(proof.publicInputs[0]) which is the nullifierHash (H(nullifier, randomness)), not the raw nullifier.
low Issues | 7 findings
Pending
#1 low Issue
Hardcoded Token Decimals Assumption
getAllPrivateBalances assumes USDC/USDT use 6 decimals and everything else uses 18. This is fragile and will produce incorrect balances for tokens with non-standard decimal counts.
Pending
#2 low Issue
Only Sepolia Supported Despite Multi-Chain UI
The app has chain switching UI and network configuration for mainnet, BSC, Polygon, and Arbitrum, but getContractAddress only supports 'sepolia'. Attempting to use another network will throw an error. This creates a confusing UX and could lead to unintended behavior.
Pending
#3 low Issue
Hardcoded Development IP Address
The development relayer URL is hardcoded to 'http://172.20.10.3:3004'. This is a specific local network IP that only works on the developer's machine. Other developers will need to change this manually.
Pending
#4 low Issue
Hardcoded API Key in Version Control
The Etherscan API key 'B1U18KNWNRF73Y58K35G2E84AN6I1W9SJZ' is hardcoded in eas.json and committed to version control. This key can be extracted from the repository and abused for rate-limited Etherscan API access.
Pending
#5 low Issue
leafIndex Always Zero — Incomplete Implementation
When saving commitments, the leafIndex is hardcoded to 0 with a TODO comment: 'TODO: Parse from contract event logs'. While this doesn't currently affect functionality (since there's no Merkle tree), it means if a Merkle tree is ever implemented, all existing commitments will have incorrect leaf indices, making them impossible to spend without migration.
Pending
#6 low Issue
Hardcoded RPC Endpoint
The RPC provider URL 'https://ethereum-sepolia-rpc.publicnode.com' is hardcoded in the service. Public RPC nodes may have rate limits, reliability issues, and can observe all queries from the app (wallet balances, transaction submissions). For a privacy-focused wallet, the RPC provider can observe which addresses the user queries, partially de-anonymizing them.
Pending
#7 low Issue
Public Signal Ordering Not Validated Before Contract Submission
The mobile app receives public signals from the backend relayer and passes them directly to the smart contract without validating their ordering or contents. The contract assumes proof.publicInputs[0] is the nullifierHash, but the mobile app does not assert this before submission. If the backend ever changes signal ordering (e.g., after circuit recompilation), transactions will fail silently or, worse, succeed with mismatched state.
informational Issues | 5 findings
Pending
#1 informational Issue
No Test Coverage
The mobile codebase contains zero test files. There are no unit tests for commitment storage, no tests for proof format conversion, no tests for the critical contract interaction flows (shield, unshield, transfer), and no tests for PIN security.
Pending
#2 informational Issue
Deprecated Method Still in Use
The shield() method is marked @deprecated in favor of shieldETH(), but it still exists and redirects. Deprecated methods should be removed after migration.
Pending
#3 informational Issue
SDK Wrapper Layer Adds Complexity Without Value
The mobile app has a VeilonSDK class in utils/sdk/ that is initialized in veilon-sdk.ts, but the actual contract interactions (shield, unshield, transfer) bypass the SDK and call the contract directly via ethers.js. The SDK layer adds indirection without providing additional functionality.
Pending
#4 informational Issue
No Monitoring, Alerting, or Error Reporting
There is no application monitoring, crash reporting (Sentry, Bugsnag, etc.), performance tracking, or analytics. Console.log is the only logging mechanism. For a financial application handling real value, this is insufficient.
Pending
#5 informational Issue
AppState Listener for Auto-Lock Not Cleaned Up
The AppState.addEventListener call in initialize() is never cleaned up (no removeEventListener). If initialize() is called multiple times, duplicate listeners accumulate, causing multiple lock() calls on each app state change.