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.
Mobile Application Analysis Statement
Application Analysis
The Veilon mobile client is a React Native (Expo) wallet that integrates with the Veilon privacy-pool smart contracts on Sepolia. It supports public ERC20 transfers, zero-knowledge shielded deposits and withdrawals, and private intra-pool transfers. Proof generation is delegated to a backend relayer; the device handles wallet custody, secret storage, transport encryption, public-input validation, and on-chain submission. The team has now remediated twenty-three of the issues raised across both audit cycles - eleven of them in this remediation round, including every Medium-class finding and the previously-High TLS pinning misconfiguration - and the codebase has matured significantly. There are no remaining Critical or High findings. The items that stay open have been formally acknowledged by the team with documented compensating controls and a remediation roadmap:
- The relayer still receives cleartext shielding secrets server-side, so a compromise of the relayer operator de-anonymises every shielded operation. The team has acknowledged this as the dominant architectural trust assumption of the current design and accepted the residual risk on the basis of the layered compensating controls (ECDH+AES authenticated transport, application-layer relayer ECDH public-key pinning, TLS SPKI pinning, and client-side publicInputs validation) and a roadmap commitment to on-device proof generation as the long-term mitigation. The finding has been re-rated from Critical to Medium in this cycle to reflect that the realistic attack surface is now reduced to relayer-operator compromise rather than an exploitable implementation flaw; Medium is the lower bound of what the residual architectural risk supports.
- Proof-level chain binding is implemented in the contract but disabled at deployment because the production circuits do not yet embed the chain identifier as a public input. The always-on deployment-chain modifier in PrivacyPoolV2 blocks cross-deployment replay unconditionally, and the team has acknowledged this contract-level mitigation as sufficient for launch; the circuit-regeneration work is tied to the same roadmap milestone as on-device proof generation so that the circuits are touched only once.
- A set of Optimization and Informational items remain open and have been formally acknowledged by the team as deferred to post-production: absence of automated test coverage for the hardened cryptographic paths, absence of crash-reporting telemetry, an unused SDK wrapper layer retained as the attachment point for the future on-device proof path, a deprecated shield() delegating stub, a hardcoded developer-only relayer URL, a leafIndex placeholder that is not consumed by the current relayer-driven proof path, an AppState listener subscription that is never released, and an imported resetHandshake helper that is never invoked on wallet switch, delete, network switch, or disconnect.
Trust Model and Privileged Components
The mobile client's security boundary is shaped by a small set of privileged components, each with explicit responsibilities and constraints:
- SecureStore holds per-wallet private keys and per-address commitment-encryption keys; access is gated by the operating-system keystore and the application-layer PIN.
- The off-chain relayer holds cleartext shielding secrets for the duration of proof generation and is authenticated both by a pinned ECDH public key at the application layer and by SHA-256 SPKI pinning at the TLS layer in production builds.
- The deployed PrivacyPoolV2 contract authorises operations through ZK-proof verification and an always-on deployment-chain modifier that blocks cross-chain replay; the client additionally validates the relayer's returned publicInputs against the circuit specification before submitting on-chain, so a malicious or buggy relayer cannot quietly swap field positions.
- PIN and biometric authentication gate wallet operations and emergency withdrawal; emergency withdrawal cannot proceed without an explicit PIN.
- The relayer is currently a single point of trust for transactional privacy and must be operated under that assumption until on-device proof generation is implemented; the team has formally acknowledged this constraint.
- Transport-layer TLS public-key pinning is now correctly configured against react-native-ssl-pinning v1.6.0 (pkPinning: true, mandatory sha256/ prefix, multiple pins to survive certificate rotation), and production builds fail closed when no pins are configured.
- Proof-level chain binding is implemented in the contract but disabled at deployment because the production circuits do not yet include the chain identifier as a public input; the contract-level deployment-chain modifier covers the launch-time threat model.
- The PIN itself is hashed with a memory-hard KDF; legacy weak hashes are migrated transparently and force-wiped at the documented migration deadline.
Security Features
The two remediation cycles introduced or hardened a substantial set of positive controls:
- PIN hashing uses scrypt (N=214, r=8, p=1) with constant-time comparison and persisted brute-force counters that survive application restarts.
- All cryptographic randomness paths fail closed when the platform CSPRNG is unavailable; the previous Math.random fallbacks have been removed across the polyfill chain and the SDK utilities.
- ECDH transport encryption between mobile and relayer is mandatory and the plaintext fallback has been removed; payloads are protected with authenticated AES-CBC + HMAC-SHA-256 over an explicit random IV (replacing the previous CryptoJS passphrase-mode construction), and the relayer's ECDH public key is pinned at the application layer in production builds.
- TLS SPKI pinning is wired into the relayer transport with mandatory sha256/-prefixed pins, support for multiple pins to survive certificate rotation, and a fail-closed production path that refuses to ship without pins configured.
- Commitment secrets, nullifiers, and randomness are AES-encrypted at rest with per-address keys held in SecureStore using the same authenticated wire format as the transport layer; legacy plaintext rows remain readable through a backward-compatibility flag, and a versioned ciphertext prefix lets the decoder distinguish new from legacy records.
- The RPC endpoint and privacy-pool contract address are no longer hardcoded: the SDK resolves the RPC URL through an explicit argument, then a build-time environment variable, then a per-chain registry, and resolves the pool address from the active network rather than the literal “sepolia”.
- A single shared toContractProof helper performs the snarkjs-to-Solidity mapping and validates the relayer-returned publicInputs against a per-circuit specification (length, positional fields, locally-known values) before any transaction is submitted, eliminating the previous duplicate inline mapping and catching field-swap attacks client-side.
- Token decimals are now captured from ERC20.decimals() at commitment-save time and persisted on the commitment, so balance display and emergency-withdraw amounts are correct for any token (WBTC = 8, USDC = 6, ETH = 18, …) instead of relying on hardcoded symbol-based assumptions.
- On-chain commitment sync now queries spentNullifiers with the Poseidon-derived nullifierHash rather than the random preimage, with BN254 field-modulus range validation, so recovery and reconciliation flows correctly mark spent commitments.
- PIN removal forwards the PIN that was just verified by the UI to the underlying service instead of submitting a hardcoded literal, restoring the intended user flow.
- The pinned-fetch wrapper no longer defaults missing response statuses to 200; malformed responses surface as errors instead of being silently treated as success.
- The previously-committed third-party block-explorer API key has been removed from eas.json; rotation of the leaked key with the provider and scrubbing the literal from version-control history are operational follow-ups outside the code-review scope, but remain necessary because the literal otherwise stays recoverable from history.
- Production logging is centralised behind a build-flag-gated logger, sensitive interpolations have been redacted, and emergency withdrawal cannot bypass PIN verification.
Auditor's Closing Remarks
The team's response across the two audit cycles has been substantive: twenty-three findings - including all four issues originally rated critical on the cryptographic and storage layers, the previously-open TLS pinning misconfiguration, and every Medium-class finding - have been properly closed. No Critical or High items remain open. The remaining ten open findings have been formally acknowledged by the team with documented compensating controls and a remediation roadmap: one Medium (the relayer trust assumption, re-rated from Critical in this cycle to reflect the layered compensating controls), one Low (proof-level chain binding, accepted on the basis of the contract-level deployment-chain modifier and tied to the same roadmap milestone as on-device proving), two Informational (a developer-only relayer URL and a leafIndex placeholder that is not consumed by the current proof path), and six Optimization items deferred to post-production. Notwithstanding the formal acknowledgments, the audit team flags two of the deferred Optimization items as warranting pre-release attention because both are single-digit-line-count fixes with disproportionate operational impact: wiring resetHandshake into the wallet-switch, delete, network-switch, and disconnect paths so a single transient ECDH handshake failure does not silently latch the relayer into a throwing state for the remainder of the session, and capturing the AppState subscription in securityStore.initialize with a teardown to prevent listener stacking. The absence of automated test coverage for the now-fail-closed cryptographic paths and the absence of crash-reporting telemetry are likewise no longer optional in spirit even where they remain optional in scope: without observability, regressions in those paths will be invisible to operators.
Note — This audit report consists of a security analysis of the Veilon mobile application source code at the revision provided for re-review. The analysis did not include dynamic penetration testing of compiled builds, in-device verification of the now-corrected TLS-pinning behaviour against a deliberately misconfigured pin in a release build, confirmation that the previously-committed API key has been rotated with the provider or scrubbed from version-control history, or formal review of the off-chain relayer service and underlying ZK circuits, which fall outside the scope of this engagement and were audited separately. We recommend the team complete the listed operational verification steps, action the two pre-release fixes flagged in the closing remarks (resetHandshake wiring and AppState teardown), integrate crash-reporting telemetry to surface any production regressions in the hardened cryptographic paths once feasible, and that users perform their own security review before transacting significant value through the application.
Files and details
Findings and Audit result
medium Issues | 6 findings
Acknowledged
#1 medium Issue
Relayer Holds Cleartext Secrets
The relayer receives the cleartext secret, nullifier, and randomness and generates the proof server-side. A compromised or coerced relayer can de-anonymise every shielded operation. ECDH transport encryption mitigates passive interception; active MITM is mitigated by TLS SPKI pinning and application-layer relayer public-key pinning.
Resolved
#2 medium Issue
TLS Certificate Pinning Misconfigured
The TLS pinning wrapper passes hashes via sslPinning.certs without setting pkPinning: true and without the sha256/ prefix. Under react-native-ssl-pinning v1.6.0 this configuration is interpreted as certificate-file pinning, not public-key pinning, so the library searches the bundle for files named after the hash values. No such files are bundled. In production this either errors out at runtime or, depending on platform fall-through, allows requests through with no pinning enforced.
Resolved
#3 medium Issue
Hardcoded RPC Endpoint
When no RPC URL is supplied, the SDK falls back to a hardcoded public Sepolia endpoint. The provider can correlate every balance and getLogs query with the wallet address.
Resolved
#4 medium Issue
Privacy Pool Address Hardcoded to Sepolia
All privacy-pool entry points pass the literal "sepolia" to getContractAddress regardless of the active network. On any non-Sepolia chain the operation either reverts or interacts with whatever contract happens to live at that address.
Resolved
#5 medium Issue
API Key Committed in eas.json
A third-party block-explorer API key is committed in eas.json for all three build profiles. Because the variable is prefixed EXPO_PUBLIC_ it is also embedded in the shipped APK. The literal value remains recoverable from git history even after rotation in HEAD.
Resolved
#6 medium Issue
Public Inputs Not Validated Before Submission
publicInputs returned by the relayer are forwarded to the contract without checking length, position, or content. A malicious or buggy relayer could swap field positions; the client would submit the proof without noticing.
low Issues | 7 findings
Acknowledged
#1 low Issue
Cross-Chain Replay — Proof-Level Binding Disabled
PrivacyPoolV2 contains both an always-on onlyDeploymentChain modifier and an optional proof-level chain binding gated by chainIdEnforced. The modifier blocks cross-deployment replay unconditionally. The proof-level binding is disabled at deployment because the current ZK circuits do not include block.chainid as a public input. Residual risk is limited to circuit-confusion attacks across verifier-sharing contracts on the same chain.
Resolved
#2 low Issue
On-Chain Nullifier Sync Uses Preimage Instead of Hash
syncCommitmentsWithBlockchain queries spentNullifiers using the random nullifier preimage stored locally in c.nullifier. The contract is keyed by the Poseidon-derived nullifier hash returned by the relayer as nullifierHash. Lookups always return false, so commitments are never marked spent through sync. Local marking of spent commitments still works, so the impact is limited to recovery and reconciliation flows.
Resolved
#3 low Issue
Duplicated snarkjs-to-Contract Proof Conversion
The snarkjs proof shape is mapped to the Solidity proof struct by two separate, slightly different inlined blocks (one in submitUnshieldTransaction, one in submitPrivateTransfer). Easy to drift; one path slices pi_a and pi_c, the other indexes directly.
Resolved
#4 low Issue
Hardcoded Token Decimals
Two call sites assume six decimals for USDC and USDT and eighteen for everything else. Tokens with other decimals (for example WBTC at eight) display incorrect balances.
Resolved
#5 low Issue
PIN Removal Always Fails Unless PIN Equals 000000
PinSecuritySettings calls removePin with the literal string "000000" after a UI-side verification step. removePin re-runs verifyPin internally and throws unless the actual PIN happens to be 000000.
Resolved
#6 low Issue
CryptoJS AES Used in Passphrase Mode
Encryption helpers pass the key to CryptoJS.AES as a string, which routes through the EvpKDF passphrase code path with CBC and no AEAD. Both call sites already feed in a high-entropy key, so the practical impact is bounded; the construction nonetheless lacks integrity protection and follows a deprecated pattern.
Resolved
#7 low Issue
Pinned Fetch Defaults Status to 200
pinnedRequest returns a status of 200 when the underlying library response has no status field. A malformed or unexpected response is reported as success to callers.
optimization Issues | 6 findings
Acknowledged
#1 optimization Issue
No Test Coverage
No test files exist in the repository. Jest is configured but unused. The recent transport, PIN, and scrypt changes ship without automated regression coverage.
Acknowledged
#2 optimization Issue
Deprecated Method Still Exported
shield() remains exported and delegates to shieldETH despite being marked deprecated. No internal callers remain.
Acknowledged
#3 optimization Issue
Unused SDK Wrapper Layer
utils/sdk/ provides a wrapper class whose proof-generation methods all throw. veilon-sdk.ts uses ethers and the relayer directly, so the wrapper adds maintenance surface without benefit.
Acknowledged
#4 optimization Issue
No Crash Reporting or Telemetry
There is no Sentry, Crashlytics, or equivalent integration. Production failures in scrypt, ECDH, and SSL pinning are invisible to operators.
Acknowledged
#5 optimization Issue
AppState Listener Subscription Leak
AppState.addEventListener in securityStore.initialize does not capture or remove the returned subscription. Repeated initialize calls (for example across hot reloads) stack listeners.
Acknowledged
#6 optimization Issue
resetHandshake Never Invoked
resetHandshake is imported in veilon-sdk but never called from setActiveWallet, deleteWallet, switchNetwork, or disconnect. After a handshake failure for the active wallet, getSharedKey throws on every subsequent call until app restart.
informational Issues | 14 findings
Resolved
#1 informational Issue
PIN Brute-Force Protection
PIN verification enforces a five-attempt lockout with a 30-second base duration and exponential backoff. Per-attempt delays escalate. Lockout state is persisted to SecureStore and survives application restarts.
Resolved
#2 informational Issue
Cryptographic Randomness Hardening
All Math.random fallbacks in cryptographic paths have been removed. Random generation now fails closed if the platform CSPRNG is unavailable.
Resolved
#3 informational Issue
Single Private-Key Storage Path
Private keys are stored exclusively per-address as wallet_pk_{address}. The legacy global key has been removed and initializeFromStorage now requires an address.
Resolved
#4 informational Issue
Contract Address Resolution
syncCommitments resolves the PrivacyPool address via getContractAddress(). No literal hex address remains.
Resolved
#5 informational Issue
Per-Wallet ECDH Cache
The ECDH shared-key cache and handshake-attempt tracker are keyed by wallet address. Switching wallets triggers a fresh handshake.
Resolved
#6 informational Issue
Plaintext Fallback Removed
encryptedPost unconditionally encrypts request bodies. Handshake failures throw instead of falling back to cleartext.
Resolved
#7 informational Issue
No Math.random in Cryptographic Paths
getRandomValues, randomFillSync, and generateSecret throw if the platform CSPRNG is unavailable. No silent downgrade to Math.random remains.
Resolved
#8 informational Issue
PIN Modal Length Alignment
The PIN verification modal supports four to six digit PINs. Auto-submit only triggers at the maximum length; an explicit Submit button is shown for shorter PINs.
Resolved
#9 informational Issue
Emergency Withdraw Requires PIN
Emergency withdraw requires PIN verification. If no PIN is set, the flow is blocked with a user-facing prompt to set one up.
Resolved
#10 informational Issue
PIN Hashing — scrypt
PIN hashing uses scrypt with parameters N=2^14, r=8, p=1 and a per-PIN random salt. Legacy iterated SHA-256 hashes migrate transparently on the next successful verify and are force-wiped after the migration deadline.
Resolved
#11 informational Issue
Centralised Logger
A central logger gates debug, info, and warn output behind __DEV__. All call sites have been migrated, and sensitive interpolations have been replaced with non-revealing messages.
Resolved
#12 informational Issue
Encrypted Commitment Storage
Commitment secret, nullifier, and randomness fields are AES-encrypted with a per-address 32-byte key from SecureStore before being written to AsyncStorage. Legacy plaintext rows are read through a backward-compatibility flag.
Acknowledged
#13 informational Issue
Hardcoded Development Relayer URL
The development relayer URL is a fixed LAN IP. The build only works on one developer machine.
Acknowledged
#14 informational Issue
leafIndex Hardcoded to Zero
Saved commitments use leafIndex = 0 with a TODO to parse the value from the Shield event log. Any future client-side proof generation that needs a Merkle path will be incorrect until this is implemented.