Veilon Info

A zk-native cross-chain wallet and protocol for private, untraceable transfers, swaps, and bridging powered by stealth addresses & encrypted transaction layers.

Veilon Logo

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.

51.54
Poor Excellent

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

Select the audit
"Static Analysis Dynamic Analysis Symbolic Execution SWC Check Manual Review"
Contract address
N/A
Network N/A
License N/A
Compiler N/A
Type N/A
Language Solidity
Onboard date 2026/02/25
Revision date 2026/02/25

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:

  1. Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
  2. Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
  3. Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
  4. Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
  5. Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
  6. Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
  7. 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 privacy pool for ETH and ERC20 deposits backed by Groth16 zero-knowledge proofs, an emergency-exit ("panic button") flow with stealth-wallet redistribution, and an on-chain governance module. The fix cycle since the previous audit closed every originally reported critical and high finding, including the seven critical issues on EmergencyExit (no proof verification, on-chain key generation, missing fund movement, unbounded contact array, unreachable cancellation, missing public-input binding) and the previously reopened MEV / front-running vector on the redesigned two-phase panic button. A small number of items still need attention before mainnet:

  • The two-phase panic button is now safe. triggerPanicButton requires the user to commit the stealth-wallet array up front and stores keccak256(abi.encodePacked(commitments)) on the session, and executeRedistribution enforces two independent gates: msg.sender == session.user || authorizedRelayers[msg.sender] AND a re-check of the stealth-commitment hash. A front-runner replaying the user's public proof can neither call the function nor substitute attacker-controlled stealth wallets.
  • Cross-chain replay protection is only partially in place. The contract-level guard (immutable deploymentChainId with an onlyDeploymentChain modifier on every state-changing entry point) blocks fork-replay, but full cross-deployment protection still requires regenerating the ZK circuits with block.chainid as the last public input and then enabling chainIdEnforced per chain. Until that work is done, a proof for one chain can in principle be replayed on another chain that holds the same commitment. The current deploy script ships with _enforceChainIdInProofs=false, consistent with the legacy circuit layout.
  • The deployment pipeline is the remaining release-blocker. scripts/deploy.js never calls privacyPool.setAuthorizedContract(emergencyExit, true) after deploying EmergencyExit, so the panic button will silently revert at PrivacyPoolV2.redistributeCommitment in production despite appearing to succeed in triggerPanicButton. The non-Sepolia scripts (deploy-arbitrum.js, deploy-local.js) and the --config hardhat.config.arbitrum.js references in package.json still target the pre-fix contract names and signatures and will fail before any bytecode is shipped. A post-deploy smoke check asserting authorizedContracts[emergencyExit] == true and deploymentChainId == network.chainid should also be added.
  • Operational control of all four contracts (PrivacyPoolV2, EmergencyExit, Verifier, Governance) is still concentrated in a single Ownable owner. The 48-hour timelock on emergency withdrawals reduces instant rug-pull risk, but moving ownership behind a multisig and a TimelockController - and migrating to Ownable2Step - is the recommended next step before mainnet.

Ownership Privileges

Ownership of every deployed contract is currently held by a single Ownable owner (the deployer). Transfer to a multisig - ideally behind a TimelockController - is recommended before mainnet. The owner retains the following privileges:

  • PrivacyPoolV2 - pause / unpause the pool, whitelist or remove supported tokens, authorize external contracts (e.g. EmergencyExit) to call redistributeCommitment, toggle ZK chain-ID enforcement, and queue / execute / cancel emergency withdrawals.
  • EmergencyExit - pause / unpause, update the emergency fee, add or remove authorized relayers, and withdraw collected fees.
  • Verifier - register, deregister, and update circuit parameters.
  • Governance - register and deregister voters, update quorum and voting period, and execute passed proposals.
  • The previous instant-drain emergencyWithdraw has been replaced with a 48-hour queue / execute / cancel timelock that emits events at every step, materially reducing rug-pull risk.
  • The panic-button redistribution is gated by a 15-minute cancellation window during which the user can abort the session, and the actual execution is restricted to the session owner or an explicitly authorized relayer - the contract owner cannot spend a user's session.
  • The "random" redistribution strategy is disabled at the contract level (fail-closed), so the validator-influenceable RNG path can no longer be reached.
  • Every owner-only setter that mutates security-critical state - setAuthorizedContract, setChainIdEnforced, setTokenWhitelist, addAuthorizedRelayer, removeAuthorizedRelayer, updateEmergencyFee, the emergency-withdraw queue / execute / cancel triplet, and the Verifier circuit registry - emits a dedicated event, so off-chain monitors can detect every privileged change.
  • The owner cannot mutate verifierContract, the underlying Groth16 verifier addresses, or deploymentChainId - all are declared immutable.

Security Features

The contracts apply a number of solid defensive practices:

  • OpenZeppelin ReentrancyGuard, Pausable, SafeERC20, and Ownable are used throughout, with nonReentrant placed first on every state-changing entry point exposed to external callers.
  • ZK verification calls real Groth16 verifiers generated by snarkJS, dispatched via a fixed-size CircuitType enum, with calldata fields equality-checked against the proof's public inputs in unshield, privateTransfer, redistributeCommitment, and triggerPanicButton.
  • unshield follows strict checks-effects-interactions ordering: nullifier marking, commitment spending, change-commitment creation, and accounting all complete before the recipient transfer; events are emitted last.
  • The panic-button flow binds caller authority and stealth-wallet identity end-to-end: phase 1 stores keccak256(abi.encodePacked(stealthCommitments)) on the session, and phase 2 re-checks both the caller (session.user or authorized relayer) and the same hash, closing the previous MEV / front-running window.
  • The custom contracts pin pragma solidity 0.8.19, declare every constructor-only reference as immutable, validate zero-address inputs in constructors, and emit events on every security-critical state change.

Note - This audit report covers the Solidity contracts in the smartcontract-main repository (PrivacyPoolV2, EmergencyExit, Verifier, Governance, and the three snarkJS-generated Groth16 verifier wrappers). The economic and tokenomic dimensions of the Veilon protocol were not analyzed. Off-chain components - the relayer service, the mobile client, the backend, and the ZK circuits themselves - were not in scope and need to be audited separately. Integrators and users should perform their own due diligence before relying on the protocol in production.

Files and details

Findings and Audit result

critical Issues | 8 findings

Resolved

#1 critical Issue
Fundamental Design Flaw
EmergencyExit.sol
L235
Description

The original triggerPanicButton and _executeRedistribution never actually transferred any ETH or tokens. The redistribution only created StealthWallet records in storage; no interaction with PrivacyPoolV2 occurred and no transfer / call / safeTransfer was invoked. The entire emergency-exit flow was a no-op for actual fund protection.

Resolved

#2 critical Issue
Privacy Violation
EmergencyExit.sol
L291
Description

The original _generateStealthWallet generated viewingKey and spendingKey on-chain and stored them in a public mapping. getSessionStealthWallets returned all wallet data, including these keys, to any caller. In a stealth-address scheme viewing and spending keys must remain private; storing private key material on-chain in plaintext defeated the entire purpose of the privacy mechanism.

Resolved

#3 critical Issue
Incomplete Implementation
EmergencyExit.sol
L398
Description

recoverStealthWallet validated the recovery proof and marked the wallet as recovered, but the actual fund transfer was never implemented - the original code contained only a comment placeholder. Even with a valid recovery proof, the user received nothing. This was an incomplete implementation deployed to testnet.

Resolved

#4 critical Issue
Missing Proof Verification
EmergencyExit.sol
L186
Description

triggerPanicButton accepted a bytes calldata _proof labelled as a 'zero-knowledge proof of fund ownership', but never verified it - the proof was merely hashed and stored. The Verifier contract was referenced but never called. Anyone with an emergency config could trigger the panic button for any arbitrary _totalAmount without proving fund ownership.

Resolved

#5 critical Issue
Unbounded Array Growth / DoS
EmergencyExit.sol
L171
Description

configureEmergencyExit pushed new emergency contacts onto config.emergencyContacts without ever clearing the existing array. Repeated calls accumulated duplicates, causing unbounded storage growth, inflating the gas cost of _notifyEmergencyContacts (which iterates the full array), and ultimately risking a denial-of-service if the array exceeded the block gas limit.

Resolved

#6 critical Issue
Unreachable Code
EmergencyExit.sol
L411
Description

cancelEmergencySession required session.status == 1 (processing). However, the original triggerPanicButton called _executeRedistribution synchronously and immediately set session.status = 2 (completed) before the function returned. By the time triggerPanicButton finished, the session was already completed and could never be cancelled, making the EMERGENCY_TIMEOUT window meaningless.

Resolved

#7 critical Issue
Proof / Public Input Binding Failure
PrivacyPoolV2.sol
L179
Description

In unshield and privateTransfer the contract verified proof.publicInputs but did not enforce that the calldata arguments (input commitment, output / change commitments, output / change amounts, token, recipient) equalled the corresponding public inputs consumed by the circuit. This broke statement-to-state binding and could allow state transitions against mismatched on-chain commitments.

Resolved

#8 critical Issue
Access Control / Front-Running / Theft of Funds
EmergencyExit.sol
L264
Description

After the two-phase redesign of the panic button, executeRedistribution (EmergencyExit.sol:264-292) is external nonReentrant whenNotPaused with no msg.sender == session.user check, no authorized-relayer check, and no requirement that the stealth commitments were committed to in phase 1. The only gates are session-pending, cancellation-window-expired, keccak256(abi.encode(_proof)) == session.proofHash, and a length match against strategy.numberOfWallets. The full _proof is publicly readable forever in the calldata of the original triggerPanicButton transaction. Concretely: once the 15-minute cancellation window closes, an attacker (or any MEV searcher) can call executeRedistribution with the user's already-public proof and their own stealth commitments. PrivacyPoolV2 marks the user's input commitment spent and creates new commitments at the attacker-supplied addresses; the attacker then unshields the funds. Even if the user tries to be the caller themselves, a competing transaction with the same proof and a higher priority fee will land first - a textbook MEV scenario. The redistributeCommitment primitive itself is fine; the failure is at the EmergencyExit layer, which gives any caller authority to spend the user's session.

high Issues | 2 findings

Resolved

#1 high Issue
Centralization / Rug Pull
PrivacyPoolV2.sol
L344
Description

The original emergencyWithdraw function let the owner withdraw any amount of any token (including ETH) with no time-lock, no multisig requirement, and no event emission. In a trust-minimized deployment this was a direct rug-pull vector: the owner could drain the entire pool instantly without warning users.

Pending

#2 high Issue
Deployment / Operational
scripts/deploy-arbitrum.js, scripts/deploy-local.js, scripts/deploy.js, package.json
L1
Description

The deployment pipeline is broken on every chain except Sepolia. package.json:11-12 references hardhat.config.arbitrum.js, which does not exist - both deploy:arbitrum-sepolia and deploy:arbitrum-mainnet, and the aggregate deploy:all-* scripts, fail before the JS runs. scripts/deploy-arbitrum.js targets contracts that no longer exist or have changed signatures: Verifier.deploy() with no args (constructor needs three), RelayerRegistry (no longer in contracts/), PrivacyPool (now PrivacyPoolV2), Governance with five args (now takes two), EmergencyExit with three args (now takes two), and a call to privacyPool.registerRelayer that no longer exists. scripts/deploy-local.js has the same drift. scripts/deploy.js produces working bytecode but never calls privacyPool.setAuthorizedContract(emergencyExit, true), so EmergencyExit.executeRedistribution will permanently revert at PrivacyPoolV2.redistributeCommitment's authorization check on every chain that uses it; the panic-button feature is silently broken even though triggerPanicButton appears to succeed and publishes the user's emergency proof in calldata. scripts/deploy.js also passes _enforceChainIdInProofs=false (line 59), so MED-02's ZK-binding mitigation is inactive on every deployment that uses this script.

medium Issues | 10 findings

Resolved

#1 medium Issue
Gas Waste / Design Flaw
Verifier.sol
L128
Description

verifyShieldProof, verifyUnshieldProof, and verifyTransferProof in Verifier.sol called this.verifyProof(...), forcing an external call back into the same contract. This wasted roughly 2600 gas per invocation, forced external instead of view visibility, and acquired the reentrancy lock on every wrapper call without a real reason.

Resolved

#2 medium Issue
Checks-Effects-Interactions Hardening
PrivacyPoolV2.sol
L225
Description

In the original unshield, the change-commitment creation and the totalUnshieldedByToken update happened after the external recipient transfer. Although nonReentrant prevented direct exploitation, the ordering still violated CEI best practices and increased the upgrade and integration risk.

Resolved

#3 medium Issue
Missing Input Validation
PrivacyPoolV2.sol
L72
Description

The PrivacyPoolV2 constructor accepted _verifierContract without checking that it was non-zero. A deployment with the zero address would have caused all unshield and privateTransfer calls to revert with opaque errors when calling the verifier. The Verifier constructor already had this check; PrivacyPoolV2 should be consistent.

Resolved

#4 medium Issue
Predictable Randomness
EmergencyExit.sol
L330
Description

The 'random' distribution strategy (type 2) used keccak256(abi.encodePacked(i, _totalAmount)) as its randomness source. Both inputs are known to the caller, making the distribution deterministic and predictable. Even when block.prevrandao and block.timestamp were folded in, those values are validator-influenceable post-Merge. The function also lacked an underflow guard for the maxAmount <= minAmount case.

Resolved

#5 medium Issue
Missing Input Validation
EmergencyExit.sol
L323
Description

The weighted distribution strategy (type 1) copied user-supplied amounts directly without verifying that their sum equalled _totalAmount. A user could specify amounts summing to more or less than the total, which would result in inconsistent accounting on-chain.

Resolved

#6 medium Issue
Architecture / Design Pattern
Verifier.sol
L81
Description

The Verifier dispatched verification requests by hashing string circuit identifiers ('shield', 'unshield'). String hashing is gas-inefficient and fragile compared to enum or selector-based dispatch, and string manipulation is an unnecessary source of risk for a security-critical router.

Resolved

#7 medium Issue
Hash Collision / Cache Key Integrity
Verifier.sol
L86
Description

verifyProof computed proofHash with abi.encodePacked over a dynamic-typed circuitType. abi.encodePacked over dynamic types can produce hash collisions across crafted packed inputs and poison the proof-cache accounting, even if it does not directly bypass Groth16 validity checks. abi.encode is the collision-safe canonical encoding.

Resolved

#8 medium Issue
Weak Randomness
EmergencyExit.sol
L281
Description

_generateStealthWallet derived stealth-wallet key material on-chain using block.timestamp, block.prevrandao, and a contract-side nonce. Each of these is predictable or validator-influenceable, so a validator or proposer could predict or manipulate the generated stealth wallet addresses. Wallet parameters should be generated off-chain or via Chainlink VRF.

Resolved

#9 medium Issue
Deprecated Pattern
EmergencyExit.sol
L500
Description

withdrawFees used payable(owner()).transfer(address(this).balance), which forwards only 2300 gas. If the owner is a multisig (e.g. Gnosis Safe) or any contract with a non-trivial receive function, the transfer reverts because 2300 gas is insufficient. The recommended replacement is .call{value: amount}(''). with a require check on the boolean return.

Resolved

#10 medium Issue
Stale Tests / Release Readiness
test/FullFlow.test.js, test/PrivateTransfer.test.js
L120
Description

Two integration tests still use outdated function signatures against the current contracts. test/FullFlow.test.js:120-126 calls unshield with 5 args, but PrivacyPoolV2.unshield now takes 7. test/FullFlow.test.js:242-247 calls privateTransfer with 4 args; the contract now takes 7. test/FullFlow.test.js:312-318 has the same 5-arg unshield bug for userB. test/PrivateTransfer.test.js:207-212 has the same 4-arg privateTransfer bug. INFO-02 fixed the equivalent issue in PrivacyPoolV2.test.js, but the rewrite was not propagated to the rest of the suite.

low Issues | 12 findings

Resolved

#1 low Issue
Dead Code
TransferVerifier_NEW.sol
L1
Description

TransferVerifier_NEW.sol contained a contract named Groth16Verifier with only 5 public inputs, while the actual TransferGroth16Verifier in TransferVerifier.sol requires 10. The file was unreferenced anywhere in the codebase and looked like leftover development scaffolding. Dead code increases the attack surface and creates confusion during audits.

Resolved

#2 low Issue
Missing Events
EmergencyExit.sol
L460
Description

Several owner-only state-changing functions did not emit events, leaving off-chain monitoring blind to critical configuration changes - notably updateEmergencyFee, addAuthorizedRelayer, removeAuthorizedRelayer, configureEmergencyExit, cancelEmergencySession, the previous emergencyWithdraw, and Verifier.updateCircuit. Events on these state transitions are essential for indexers, alerting, and incident response.

Resolved

#3 low Issue
Gas Optimization / Immutability
PrivacyPoolV2.sol
L66
Description

Several state variables that were only set in the constructor were declared as regular storage. Declaring them immutable saves roughly 2100 gas per SLOAD and enforces that they cannot be changed after deployment - a useful invariant for security-critical references like the verifier contract.

Resolved

#4 low Issue
Modifier Ordering
PrivacyPoolV2.sol
L97
Description

Multiple functions placed nonReentrant after other modifiers in the modifier list. If any earlier modifier had a reentrancy issue, the guard would not protect against it. nonReentrant should be applied first so the lock is acquired before any other modifier logic runs.

Resolved

#5 low Issue
Unused State Variable
PrivacyPoolV2.sol
L51
Description

PrivacyPoolV2 maintained a Counters.Counter _transactionId that was incremented on every shield and on every commitment creation but was never read externally - no getter exposed it and no on-chain logic depended on it. Each increment cost an SSTORE for no functional benefit.

Resolved

#6 low Issue
Logic Error
Governance.sol
L51
Description

The Voter struct in Governance had a hasVoted field that was never set to true after a voter cast a ballot. The getVoter view always returned false for this field, even when the voter had clearly voted. Per-proposal voting state was correctly tracked through Receipt.hasVoted, but the voter-level field was misleading.

Resolved

#7 low Issue
Logic / Semantics
Governance.sol
L312
Description

The proposalPassed view returned false once a proposal had been executed. This was semantically misleading for integrators - an executed proposal has, by definition, passed. Either rename the function to indicate pending-execution state, or return the pass status independently of execution.

Resolved

#8 low Issue
Unspecific Pragma
PrivacyPoolV2.sol
L2
Description

All custom contracts used a floating pragma (^0.8.19), allowing compilation under any 0.8.x version at or above 0.8.19. Different compiler versions can produce different bytecode and have different bugs. Pinning the pragma for production deployments ensures deterministic compilation and clean source verification.

Resolved

#9 low Issue
Access Control
MockERC20.sol
L25
Description

MockERC20.mint() and burn() had no access control. Although a test contract, it was placed alongside production code in the contracts/ directory, so an accidental mainnet deployment would have allowed anyone to mint unlimited tokens.

Resolved

#10 low Issue
Missing Events
PrivacyPoolV2.sol
L140
Description

Two owner-only setters introduced during the fix cycle change security-critical state without emitting events. setChainIdEnforced (PrivacyPoolV2.sol:140) toggles the cross-chain replay defense, and setAuthorizedContract (line 149) grants the privilege to call redistributeCommitment - the same privilege abused by NEW-01. Off-chain monitoring is currently blind to either change.

Resolved

#11 low Issue
Operational Bug
scripts/deploy.js
L197
Description

deploy.js stores the deployed pool address under deployedContracts.PrivacyPoolV2 (line 62), but updateEnvironmentFile reads contracts.PrivacyPool (line 197). The rendered env line therefore becomes PRIVACY_POOL_ADDRESS=undefined, silently breaking any downstream consumer (relayer, mobile app, backend) that depends on env.example.

Acknowledged

#12 low Issue
Cross-Chain Replay
PrivacyPoolV2.sol
L179
Description

ZK proof verification did not include block.chainid or any chain-specific identifier in the public inputs. Since the contracts are deployed on multiple chains (Sepolia, Arbitrum, BSC, Mumbai), a valid proof generated on one chain could be replayed on another chain where the same commitment exists. Either bind block.chainid into the ZK circuit's public inputs or include the deployment address in the nullifier derivation.

optimization Issues | 4 findings

Resolved

#1 optimization Issue
Gas Optimization
Verifier.sol
L283
Description

In _verifyProofByType, keccak256(abi.encodePacked(circuitType)) was computed up to three times to compare against 'shield', 'unshield', and 'transfer'. Each keccak256 over a string costs gas and the pattern was inherently unsafe compared to a fixed-size enum.

Resolved

#2 optimization Issue
Gas Optimization
EmergencyExit.sol
L171
Description

In configureEmergencyExit, the loop used config.emergencyContacts.push() which performs SSTORE on every iteration, and re-read _contacts.length from calldata on every iteration. Caching the length and considering batched storage patterns reduces gas cost and avoids redundant calldata reads.

Resolved

#3 optimization Issue
Gas Optimization
PrivacyPoolV2.sol
L51
Description

PrivacyPoolV2 used a Counters.Counter _transactionId that was incremented on every shield and on every commitment creation. Each increment cost roughly 5000 gas (non-zero to non-zero SSTORE) without any external read of the value. Either remove it or replace it with a plain uint256 increment, which is cheaper than the Counters library.

Resolved

#4 optimization Issue
Redundant Storage
PrivacyPoolV2.sol
L113
Description

Commitment data was stored redundantly: once in the shieldedTransactions mapping (which already carries token, amount, etc.) and again in separate commitmentAmount and commitmentToken mappings. This doubled SSTORE costs on every shield operation.

informational Issues | 6 findings

Resolved

#1 informational Issue
Broken Test
PrivacyPoolV2.test.js
L189
Description

The test labelled 'Should track total shielded correctly' called privacyPool.totalShielded(), a function that does not exist on the contract. The contract uses the totalShieldedByToken(token) mapping instead. The test was written against an older interface and was never updated.

Resolved

#2 informational Issue
Broken Test
PrivacyPoolV2.test.js
L229
Description

The test labelled 'Should allow user to unshield ETH' called unshield with the old 5-parameter signature (nullifier, recipient, amount, token, proof). The current contract requires 7 parameters (inputCommitment, changeCommitment, recipient, outputAmount, changeAmount, token, proof), so the test never exercised the current interface.

Pending

#3 informational Issue
External Dependency
FullFlow.test.js
L11
Description

FullFlow.test.js and PrivateTransfer.test.js depend on a relayer service running at localhost:3004 which is not included in the repository. As a result these integration tests cannot be executed in isolation, which weakens the safety net for the most realistic end-to-end flows.

Pending

#4 informational Issue
Missing Test Coverage
PrivacyPoolV2.sol
L1
Description

Several critical code paths still have no test coverage: partial unshield with change commitment, private transfer with change, the emergency-withdraw queue/execute/cancel flow, all Governance flows (voting, execution, quorum), the redesigned EmergencyExit recovery and cancellation paths, access-control violation attempts, token de-whitelisting while funds are shielded, and reentrancy attack simulations.

Pending

#5 informational Issue
Multiple Solidity Versions
ShieldVerifier.sol
L21
Description

Four different Solidity version constraints are used across the project: ^0.8.19 in the custom contracts, ^0.8.0 and ^0.8.1 in OpenZeppelin, and >=0.7.0 <0.9.0 in the snarkJS-generated verifiers (ShieldVerifier.sol, UnshieldVerifier.sol, TransferVerifier.sol). The snarkJS verifiers in particular use a very wide pragma range; even though the code is auto-generated, pinning it tightens the build.

Pending

#6 informational Issue
Centralization Risk
Governance.sol
L194
Description

All four contracts (PrivacyPoolV2, EmergencyExit, Verifier, Governance) rely on a plain Ownable owner. The owner can pause the pool, whitelist tokens, queue and execute emergency withdrawals, register and deregister voters, execute proposals, and register / deregister verifier circuits. This concentrates significant operational control in a single key and is the largest residual trust assumption in the system.