View Audit Report
This document presents the results of the security review conducted by Nethermind Security for Panoptic's HypoVault contracts. The HypoVault provides a decentralized asset management framework that allows users to deposit funds into on-chain investment vehicles. These vaults are actively managed by designated strategists who execute trading strategies to generate yield. The protocol uses an asynchronous, epoch-based system for deposits and withdrawals to ensure fair pricing for all participants.
This audit focused on the smart contracts that implement the protocol's core logic, including the HypoVault contract, which holds assets and issues shares; the PanopticVaultAccountant, responsible for secure Net Asset Value (NAV) calculation; the HypoVaultFactory, which enables the permissionless creation of new vaults; and the HypoVaultManagerWithMerkleVerification, which facilitates trust-minimized strategy execution.
The audit comprises 690 lines of Solidity code. The audit was performed using (a) manual analysis of the codebase, and (b) automated analysis tools.
| Audit assessment | |
| Audit Type | Security Review |
| Initial Report | August 25, 2025 |
| Final Report | June 17, 2026 |
| Initial commit | dc89b9ce4f |
| Final commit | a00f269df2 |
| Documentation Assessment | High |
| Test Suite Assessment | High |
Findings by severity
| # | Contract | LoC | Comments | Ratio | Blank | Total |
| 1 | src/HypoVault.sol | 384 | 189 | 49.2% | 144 | 717 |
| 2 | src/HypoVaultFactory.sol | 37 | 24 | 64.9% | 6 | 67 |
| 3 | src/managers/HypoVaultManagerWithMerkleVerification.sol | 48 | 24 | 50.0% | 12 | 84 |
| 4 | src/interfaces/IVaultAccountant.sol | 8 | 6 | 75.0% | 1 | 15 |
| 5 | src/interfaces/IHypoVault.sol | 22 | 2 | 9.1% | 7 | 31 |
| 6 | src/accountants/PanopticVaultAccountant.sol | 191 | 54 | 28.3% | 40 | 285 |
| Total | 690 | 299 | 43.3% | 210 | 1199 |
* The review of the PanopticVaultAccountant.sol contract was limited in scope. A complete analysis was not performed, as it would require an in-depth understanding of the core Panoptic protocol, which was outside the scope of this engagement.
| # | Finding | Severity | Update |
| 1 | Rounding error in executeDeposit(...) may lead to state inconsistency and excessive share minting | Low | Acknowledged |
| 2 | Rounding error in share distribution leads to locked yield | Low | Acknowledged |
| 3 | Manager has exclusive control over deposit cancellations | Info | Fixed |
| 4 | The decimals(...) function returns a misleading value of 0 for underlying tokens that do not implement it | Info | Acknowledged |
The Panoptic Vault protocol provides a framework for decentralized asset management through its core component, the HypoVault. It is designed to separate the roles of capital providers (Depositors) from strategy executors (Managers). Users can deposit assets to gain passive exposure to complex, actively managed trading strategies. The system employs an asynchronous, epoch-based model for deposits and withdrawals to ensure fair pricing and mitigate timing-based advantages. New vaults can be created permissionlessly by anyone using the provided factory contract.
HypoVault ContractThe HypoVault is the central contract that serves as both the asset custodian and the on-chain representation of ownership. It functions as an ERC20 token, where each token represents a share of the vault's total assets. The contract is responsible for managing the lifecycle of user funds through a queuing system.
depositEpoch, withdrawalEpoch, depositEpochState, and withdrawalEpochState are used to segregate and process user requests in batches.queuedDeposit and queuedWithdrawal) to record individual user requests for each epoch, ensuring that assets and shares are allocated on a pro-rata basis during fulfillment.totalSupply() equals the sum of all individual user balances. This occurs because the logic that updates the totalSupply is separate from the logic that mints shares to individual user accounts. For instance, the total supply may be increased in one step to reflect a batch of new deposits, while the corresponding shares are distributed to users in a subsequent, distinct operation.PanopticVaultAccountantThe PanopticVaultAccountant is a dedicated, external valuation engine responsible for calculating the vault's Net Asset Value (NAV). This separation of concerns is a critical security feature, as it prevents a potentially malicious Manager from manipulating the vault's reported value. Its primary function, computeNAV(), is called during the fulfillment of deposits and withdrawals. The valuation process uses a hybrid model that combines manager-provided data with on-chain verification.
managerInput) containing off-chain information such as a complete list of the vault's positions and current price quotes for the assets.The protocol establishes a clear separation of powers between the Manager and the Curator to enhance security and operational integrity.
manage() function and processing user fund flows by calling fulfillDeposits() and fulfillWithdrawals(). A contract such as HypoVaultManagerWithMerkleVerification is intended to be set as the vault's manager. This contract acts as a proxy, allowing a Curator to grant restricted, role-based access to multiple strategists. These strategists can only execute actions that are part of a pre-approved Merkle root, enforcing a trust-minimized operational model.PanopticVaultAccountant and manager contracts. They are responsible for the initial security setup, defining the approved list of pools the vault can interact with, and setting key risk parameters such as maximum price deviation for oracles.requestWithdrawalFrom(). This can be used to eject a user from the vault or to trigger "fee collection" by setting a shouldRedeposit flag. In the latter case, the user's accrued profits are realized, performance fees are collected, and the funds are automatically re-deposited into the next epoch.The system is designed to be extensible and follows a structured, asynchronous process for handling user funds.
HypoVaultFactory contract allows any user to permissionlessly deploy a new, independent HypoVault instance. The creator specifies the vault's underlying asset, manager, accountant, and performance fee structure. The factory emits a VaultCreated event, which provides an on-chain registry for discovering new vaults.executeDeposit() or executeWithdrawal()) to claim their corresponding shares or assets. Any portion of their request that was not fulfilled by the Manager is automatically rolled over to the next epoch's queue.The risk rating methodology used by Nethermind Security follows the principles established by the OWASP Foundation. The severity of each finding is determined by two factors: Likelihood and Impact.
Likelihood measures how likely the finding is to be uncovered and exploited by an attacker. This factor will be one of the following values:
When defining the likelihood of a finding, other factors are also considered. These can include but are not limited to motive, opportunity, exploit accessibility, ease of discovery, and ease of exploit.
Impact is a measure of the damage that may be caused if an attacker exploits the finding. This factor will be one of the following values:
When defining the impact of a finding, other factors are also considered. These can include but are not limited to data and state integrity, loss of availability, financial loss, and reputation damage. After defining the likelihood and impact of an issue, the severity is determined according to the table below.
| Impact / Likelihood | Low | Medium | High |
| High | Medium | High | Critical |
| Medium | Low | Medium | High |
| Low | Info / Best Practices | Low | Medium |
| Undetermined | Undetermined | Undetermined | Undetermined |
To address issues that do not fit a High, Medium or Low severity, Nethermind Security also uses three more finding severities: Informational, Best Practices, and Undetermined.
executeDeposit(...) may lead to state inconsistency and excessive share minting| File | Fix commit | Type | Status |
src/HypoVault.sol | — | Low | Acknowledged |
The vault's deposit mechanism is spread across three functions: requestDeposit(...), where users queue their assets; fulfillDeposits(...), where the manager finalizes an epoch by specifying the total assets to be invested; and executeDeposit(...), where users claim their shares based on the fulfilled amount.
In the executeDeposit(...) function, the portion of a user's deposit that gets fulfilled ( userAssetsDeposited) is calculated pro-rata using Math.mulDiv(...). Any of the user's assets that are not fulfilled ( assetsRemaining) are carried over to the next deposit epoch.
function executeDeposit(address user, uint256 epoch) external {
if (epoch >= depositEpoch) revert EpochNotFulfilled();
uint256 queuedDepositAmount = queuedDeposit[user][epoch];
queuedDeposit[user][epoch] = 0;
DepositEpochState memory _depositEpochState = depositEpochState[epoch];
// @audit-issue Due to downward rounding, the sum of all `userAssetsDeposited`
// across all users will be less than `_depositEpochState.assetsFulfilled`.
uint256 userAssetsDeposited = Math.mulDiv(
queuedDepositAmount,
_depositEpochState.assetsFulfilled,
_depositEpochState.assetsDeposited
);
// ...
// @audit The unfulfilled portion of the deposit is carried over to the user's balance in the next epoch.
uint256 assetsRemaining = queuedDepositAmount - userAssetsDeposited;
if (assetsRemaining > 0) queuedDeposit[user][epoch + 1] += uint128(assetsRemaining);
// ...
}The issue is that Math.mulDiv(...) rounds down. Consequently, the sum of all userAssetsDeposited across all depositors in an epoch will be slightly less than the total assetsFulfilled defined by the manager.
This dust is problematic because while the assetsRemaining for each user (including their share of the dust) is carried over to the next epoch in the queuedDeposit mapping, the global state is not updated correspondingly. In fulfillDeposits(...), the next epoch's total assetsDeposited is calculated simply as the total unfulfilled assets, without accounting for the accumulated user-level dust from the previous epoch's rounding errors.
function fulfillDeposits(...) external onlyManager {
// ...
// @audit The global remaining assets are calculated here.
uint256 assetsRemaining = epochState.assetsDeposited - assetsToFulfill;
// ...
currentEpoch++;
depositEpoch = uint128(currentEpoch);
// @audit The next epoch's global deposit amount is set. This value will be smaller than the
// sum of all users' queued deposits due to the rounding errors from the prior epoch.
depositEpochState[currentEpoch] = DepositEpochState({
assetsDeposited: uint128(assetsRemaining),
sharesReceived: 0,
assetsFulfilled: 0
});
// ...
}This creates a state inconsistency where the sum of individual queued deposits for an epoch is greater than the globally tracked depositEpochState[epoch].assetsDeposited. This inconsistency can cause an excessive number of shares to be minted. In the subsequent epoch, the denominator for the pro-rata calculation ( _depositEpochState.assetsDeposited) will be smaller than the actual sum of queued assets (the numerator). This inflates the fulfillment ratio, causing more shares to be minted than are backed by assets and diluting the value of all existing shareholders in the vault.
Recommendation
Consider modifying the current design to avoid this or consistently monitor the state of vaults to act quickly before the accumulation of this dust can become a problem.
Resolution
The underlying cause is that partial deposit fulfillments round down per user, while the next epoch’s global deposited amount only carries forward the aggregate remainder. This creates a mismatch between user-level queued deposits and the global epoch denominator. That said, the practical impact is limited to dust-level drift in the underlying asset’s smallest unit. For assets like USDC, the maximum mismatch per partially fulfilled epoch is bounded by the number of depositors minus one base unit, making economically meaningful extraction infeasible on Ethereum mainnet absent an unrealistic number of sybil depositors. We therefore acknowledge the issue as a real accounting edge case, but do not view it as a credible loss-of-funds or protocol-bricking risk under expected operating conditions.
A concrete USDC example shows the scale. If three users each deposit 1 USDC and only 2 USDC is fulfilled, each user is credited 0.666666 USDC and rolls 0.333334 USDC. The true rollover total is 1.000002 USDC, while the vault globally records 1.000000 USDC, creating a mismatch of only 0.000002 USDC total. If the same cohort is fully processed on the next fulfillment, that dust offsets exactly across that same cohort. A transfer only appears if the shareholder set changes between the partial fulfillment and the later rollover settlement, such as if new users deposit in between.
The magnitude is extremely small in native asset units. For USDC, this example is 2 x 10-6 USDC, which is six orders of magnitude below 1 USDC. For WETH, an analogous two-unit mismatch would be 2 x 10-18 WETH, which is eighteen orders of magnitude below 1 WETH. More generally, the drift is bounded by the number of depositors minus one in base units, so the impact depends on the number of distinct depositor addresses in a partially fulfilled epoch, not on the raw share-unit count.
From an attack perspective, an attacker would need many distinct depositor addresses all participating in a partially fulfilled epoch in order to accumulate any measurable benefit, and even then the result would only be dust. If assuming the vault managers fulfill on a regular cadence, the expected small depositor count per epoch, and the fact that partial fulfillment only occurs when the cap is hit and is then resolved on the next cycle, such an attack is extremely unlikely and economically unprofitable on Ethereum mainnet.
| File | Fix commit | Type | Status |
src/HypoVault.sol | — | Low | Acknowledged |
In the vault's deposit lifecycle, the fulfillDeposits(...) function calculates the total number of new shares to be created for an epoch and increases the contract's totalSupply accordingly. Subsequently, depositors call the executeDeposit(...) function to mint their individual portion of these new shares.
A problem arises in the executeDeposit(...) function where individual share amounts are calculated. The calculation uses Math.mulDiv(...), which rounds down.
function executeDeposit(address user, uint256 epoch) external {
// ...
// @audit-issue The downward rounding here means the sum of shares received by all
// users can be less than the total shares allocated for the epoch.
uint256 sharesReceived = Math.mulDiv(
userAssetsDeposited,
_depositEpochState.sharesReceived,
_depositEpochState.assetsFulfilled == 0 ? 1 : _depositEpochState.assetsFulfilled
);
_mintVirtual(user, sharesReceived);
// ...
}Due to this downward rounding, the sum of all shares minted to individual users throughout an epoch may be less than the total amount by which the totalSupply was increased in fulfillDeposits(...).
function fulfillDeposits(...) external onlyManager {
// ...
uint256 _totalSupply = totalSupply;
uint256 sharesReceived = Math.mulDiv(assetsToFulfill, _totalSupply, totalAssets);
// ...
// @audit The totalSupply is increased by the full calculated amount,
// before individual (rounded-down) shares are minted to users.
totalSupply = _totalSupply + sharesReceived;
emit DepositsFulfilled(currentEpoch - 1, assetsToFulfill, sharesReceived);
}This creates a persistent discrepancy where the totalSupply is greater than the sum of all user balances. The resulting difference is a growing number of shares that are not owned by any address.
Since the vault's Net Asset Value (NAV) and any generated profits are divided across the entire totalSupply, these shares are entitled to a portion of the vault's assets. This portion becomes locked in the contract, as there is no owner who can claim or redeem these shares. The portion of locked funds will growth with every new deposit epoch, continuously eroding the returns for legitimate shareholders.
Recommendation
Consider implementing a mechanism to handle the rounding dust from the share calculation in executeDeposit(...). For example, the last user to execute their deposit in an epoch receives any remaining fractional shares, thus aligning the sum of minted balances with the increase in totalSupply.
Resolution
The vault increases total supply for the full batch at fulfillment time, while per-user share allocations during execution are rounded down. This can leave a very small number of unowned shares in the system, which slightly dilutes returns over time. In practice this is a dust-level effect and not realistically exploitable for meaningful value extraction. We acknowledge the issue, but view the impact as negligible.
| File | Fix commit | Type | Status |
src/HypoVault.sol | a00f269 | Info | Fixed |
When users deposit assets into the HypoVault via the requestDeposit(...) function, their tokens are immediately transferred to and held by the contract. These funds remain in a pending state until processed by the vault's manager.
The contract provides a cancelDeposit(...) function to return these pending assets to the user. However, this function includes an onlyManager modifier, making the manager the only entity with the authority to execute a cancellation.
// @audit Only the manager can cancel a deposit.
function cancelDeposit(address depositor) external onlyManager {
uint256 currentEpoch = depositEpoch;
uint256 queuedDepositAmount = queuedDeposit[depositor][currentEpoch];
queuedDeposit[depositor][currentEpoch] = 0;
depositEpochState[currentEpoch].assetsDeposited -= uint128(queuedDepositAmount);
SafeTransferLib.safeTransfer(underlyingToken, depositor, queuedDepositAmount);
emit DepositCancelled(depositor, queuedDepositAmount);
}This design places complete control over pending deposits with the manager role. Users cannot unilaterally retrieve their funds after depositing them. The return of unfulfilled assets is contingent upon the manager's action, creating a strong trust assumption regarding the manager's liveness and integrity.
Recommendation
The current design centralizes control over pending user funds. For future consideration, the team might evaluate introducing a permissionless way for users to cancel their own deposits.
Resolution
Fixed in commit a00f26. Users can now cancel their own pending deposits via the permissionless cancelDeposit(...) path, so the trust/liveness concern identified in the audit has been addressed.
decimals(...) function returns a misleading value of 0 for underlying tokens that do not implement it| File | Fix commit | Type | Status |
src/HypoVault.sol | — | Info | Acknowledged |
The HypoVault contract implements the ERC20 standard, representing shares in the vault. Its decimals(...) function is designed to mirror the decimals of the underlyingToken to ensure consistency.
The current implementation uses a try...catch statement to fetch the decimals from the underlying token. If the call to underlyingToken.decimals() fails—for instance, if the token contract does not implement this function—the catch block returns 0.
function decimals() external view returns (uint8) {
try IERC20Metadata(underlyingToken).decimals() returns (uint8 _decimals) {
return _decimals;
// @audit-issue If the underlying token does not implement decimals(), this returns 0.
} catch {
return 0;
}
}The decimals() function is an optional part of the ERC20 standard. While a return value of 0 is valid and indicates a non-divisible token.
Returning 0 when the function is not implemented is misleading and can cause integration issues.
Recommendation
Consider modifying the behavior for underlying tokens that do not implement the decimals() function. Instead of returning 0, a different approach could be to revert the transaction to make the incompatibility explicit or set the decimals during initialization.
Resolution
This behavior may be misleading for integrators or user interfaces interacting with non-standard ERC20 tokens, but it does not create an onchain loss-of-funds scenario. We acknowledge the issue as a compatibility/documentation concern rather than a material protocol risk.
Software documentation refers to the written or visual information that describes the functionality, architecture, design, and implementation of software. It provides a comprehensive overview of the software system and helps users, developers, and stakeholders understand how the software works, how to use it, and how to maintain it. Software documentation can take different forms, such as user manuals, system manuals, technical specifications, requirements documents, design documents, and code comments. Software documentation is critical in software development, enabling effective communication between developers, testers, users, and other stakeholders. It helps to ensure that everyone involved in the development process has a shared understanding of the software system and its functionality. Moreover, software documentation can improve software maintenance by providing a clear and complete understanding of the software system, making it easier for developers to maintain, modify, and update the software over time. Smart contracts can use various types of software documentation. Some of the most common types include:
These types of documentation are essential for smart contract development and maintenance. They help ensure that the contract is properly designed, implemented, and tested, and they provide a reference for developers who need to modify or maintain the contract in the future.
The Panoptic team provided comprehensive documentation that clearly outlined the system's architecture, core components, and intended functionalities. The codebase itself is well-documented, which greatly facilitated the audit process. Furthermore, the team promptly and thoroughly addressed all questions raised by the Nethermind Security team, providing valuable insights and a deeper understanding of the protocol's technical aspects.
| 175Tests passed | 0Failed |
The complete test output is in the full report.
As auditors, we recommend that any changes or updates made to the audited codebase undergo a re-audit or security review to address potential vulnerabilities or risks introduced by the modifications. By conducting a re-audit or security review of the modified codebase, you can significantly enhance the overall security of your system and reduce the likelihood of exploitation. However, we do not possess the authority or right to impose obligations or restrictions on our clients regarding codebase updates, modifications, or subsequent audits. Accordingly, the decision to seek a re-audit or security review lies solely with you.
This report is based on the scope of materials and documentation provided by you to Nethermind in order that Nethermind could conduct the security review outlined in 1. Executive Summary and 2. Audited Files. The results set out in this report may not be complete nor inclusive of all vulnerabilities. Nethermind has provided the review and this report on an as-is, where-is, and as-available basis. You agree that your access and/or use, including but not limited to any associated services, products, protocols, platforms, content, and materials, will be at your sole risk. Blockchain technology remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond the programming language, or other programming aspects that could present security risks. This report does not indicate the endorsement of any particular project or team, nor guarantee its security. No third party should rely on this report in any way, including for the purpose of making any decisions to buy or sell a product, service or any other asset. To the fullest extent permitted by law, Nethermind disclaims any liability in connection with this report, its content, and any related services and products and your use thereof, including, without limitation, the implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Nethermind does not warrant, endorse, guarantee, or assume responsibility for any product or service advertised or offered by a third party through the product, any open source or third-party software, code, libraries, materials, or information linked to, called by, referenced by or accessible through the report, its content, and the related services and products, any hyperlinked websites, any websites or mobile applications appearing on any advertising, and Nethermind will not be a party to or in any way be responsible for monitoring any transaction between you and any third-party providers of products or services. As with the purchase or use of a product or service through any medium or in any environment, you should use your best judgment and exercise caution where appropriate. FOR AVOIDANCE OF DOUBT, THE REPORT, ITS CONTENT, ACCESS, AND/OR USAGE THEREOF, INCLUDING ANY ASSOCIATED SERVICES OR MATERIALS, SHALL NOT BE CONSIDERED OR RELIED UPON AS ANY FORM OF FINANCIAL, INVESTMENT, TAX, LEGAL, REGULATORY, OR OTHER ADVICE.
The Leading Engineers of Blockchain Infrastructure