Panoptic (HypoVault)

HypoVault is Panoptic’s framework for on-chain vaults run by strategists. We focused on how deposits and withdrawals are priced across epochs and how strategist actions are limited to approved, Merkle-verified calls.
report id
0615
category
DeFi & Yield
language
Solidity
final report
June 17, 2026
lines of code
690
Content

View Audit Report

1

Executive Summary

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 TypeSecurity Review
Initial ReportAugust 25, 2025
Final ReportJune 17, 2026
Initial commitdc89b9ce4f
Final commita00f269df2
Documentation AssessmentHigh
Test Suite AssessmentHigh
Total findings
4
Fixed
1
Acknowledged
3
Mitigated
0
Unresolved
0

Findings by severity

  • Critical0
  • High0
  • Medium0
  • Low2
  • Undetermined0
  • Informational2
  • Best Practices0

2

Audited Files

#ContractLoCCommentsRatioBlankTotal
1src/HypoVault.sol38418949.2%144717
2src/HypoVaultFactory.sol372464.9%667
3src/managers/HypoVaultManagerWithMerkleVerification.sol482450.0%1284
4src/interfaces/IVaultAccountant.sol8675.0%115
5src/interfaces/IHypoVault.sol2229.1%731
6src/accountants/PanopticVaultAccountant.sol1915428.3%40285
Total69029943.3%2101199

* 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.

4

System Overview

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.

4.1 The HypoVault Contract

The 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.

  • Asset Custody: It holds all assets deposited by users. These funds can only be deployed externally through strictly controlled functions callable by the designated Manager.
  • Epoch Management: It tracks the state of deposits and withdrawals through a series of epochs. Key state variables like depositEpoch, withdrawalEpoch, depositEpochState, and withdrawalEpochState are used to segregate and process user requests in batches.
  • User Queues: It maintains mappings (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.
  • ERC-20 Considerations: The contract may not always adhere to the common ERC-20 behavior where 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.
  • Token Compatibility: The vault's internal accounting is designed for standard ERC20 tokens. It is not compatible with fee-on-transfer or rebasing tokens, as these can cause discrepancies between the assets transferred and the amount recorded, leading to state inconsistencies. It is also not compatible with tokens with huge balances.

4.2 The PanopticVaultAccountant

The 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.

  • Manager-Provided Data: The Manager submits a payload of data (managerInput) containing off-chain information such as a complete list of the vault's positions and current price quotes for the assets.
  • On-Chain Verification: The accountant contract performs several critical on-chain safety checks before finalizing the NAV calculation:
    • It verifies that the hash of the provided pool information matches a pre-approved configuration set by the Curator, reverting if there is a mismatch.
    • It ensures the list of positions provided by the Manager is complete by comparing it against the vault's actual on-chain holdings.
    • It validates the Manager's price quotes against on-chain Time-Weighted Average Price (TWAP) oracles, reverting if the deviation exceeds a pre-configured threshold.

4.3 The Manager and Curation Framework

The protocol establishes a clear separation of powers between the Manager and the Curator to enhance security and operational integrity.

  • Manager: The Manager is the active strategist responsible for executing trades via the 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.
  • Curator: The Curator is an administrative role, typically the owner of the 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.
  • Forced Withdrawals: The Manager has the authority to initiate a withdrawal on behalf of any user by calling 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.

4.4 Permissionless Deployment and Fund Management

The system is designed to be extensible and follows a structured, asynchronous process for handling user funds.

  • Factory: The 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.
  • Epoch-Based Flow: Deposits and withdrawals are not processed instantly but are batched into epochs. This ensures all participants in a given batch receive a uniform price based on a single NAV calculation, protecting them from front-running. The flow for both deposits and withdrawals consists of three distinct phases:
    1. Request Phase: A user initiates a deposit or withdrawal, and their request is added to the queue for the current, unfulfilled epoch.
    2. Fulfillment Phase: The Manager closes the current epoch by calling the appropriate fulfillment function. This triggers the NAV calculation, determines the share-to-asset conversion rate for that epoch, and updates the vault's total supply.
    3. Execution Phase: After an epoch is fulfilled, users must call an execution function (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.

5

Risk Rating Methodology

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:

  1. High: the issue is trivial to exploit and has no specific conditions that need to be met;
  2. Medium: the issue is moderately complex and may have some conditions that need to be met;
  3. Low: the issue is very complex and requires very specific conditions to be met.

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:

  1. High: the issue can cause significant damage, such as loss of funds or the protocol entering an unrecoverable state;
  2. Medium: the issue can cause moderate damage, such as impacts that only affect a small group of users or only a particular part of the protocol;
  3. Low: the issue can cause little to no damage, such as bugs that are easily recoverable or cause unexpected interactions that cause minor inconveniences.

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 / LikelihoodLowMediumHigh
HighMediumHighCritical
MediumLowMediumHigh
LowInfo / Best PracticesLowMedium
UndeterminedUndeterminedUndeterminedUndetermined

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.

  1. Informational findings do not pose any risk to the application, but they carry some information that the audit team intends to pass to the client formally;
  2. Best Practice findings are used when some piece of code does not conform with smart contract development best practices;
  3. Undetermined findings are used when we cannot predict the impact or likelihood of the issue.

6

Issues

6.1 Rounding error in executeDeposit(...) may lead to state inconsistency and excessive share minting

FileFix commitTypeStatus
src/HypoVault.sol—LowAcknowledged

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.

6.2 Rounding error in share distribution leads to locked yield

FileFix commitTypeStatus
src/HypoVault.sol—LowAcknowledged

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.

6.3 Manager has exclusive control over deposit cancellations

FileFix commitTypeStatus
src/HypoVault.sola00f269InfoFixed

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.

6.4 The decimals(...) function returns a misleading value of 0 for underlying tokens that do not implement it

FileFix commitTypeStatus
src/HypoVault.sol—InfoAcknowledged

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.

7

Documentation Evaluation

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:

  • Technical whitepaper: A technical whitepaper is a comprehensive document describing the smart contract’s design and technical details. It includes information about the purpose of the contract, its architecture, its components, and how they interact with each other;
  • User manual: A user manual is a document that provides information about how to use the smart contract. It includes step-by-step instructions on how to perform various tasks and explains the different features and functionalities of the contract;
  • Code documentation: Code documentation is a document that provides details about the code of the smart contract. It includes information about the functions, variables, and classes used in the code, as well as explanations of how they work;
  • API documentation: API documentation is a document that provides information about the API (Application Programming Interface) of the smart contract. It includes details about the methods, parameters, and responses that can be used to interact with the contract;
  • Testing documentation: Testing documentation is a document that provides information about how the smart contract was tested. It includes details about the test cases that were used, the results of the tests, and any issues that were identified during testing;
  • Audit documentation: Audit documentation includes reports, notes, and other materials related to the security audit of the smart contract. This type of documentation is critical in ensuring that the smart contract is secure and free from vulnerabilities.

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.

8

Remarks & Test Suite

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.

Test suite
175Tests passed0Failed

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.