Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · July 2025

Universal Vault

for Universal

Alongside engaged Guardian to review the security of their Universal Vault allowing users to deposit and earn yield from customized strategies. From the 7th of July to the 14th of July, a team of 5 auditors reviewed the source code in scope.

Published
Review window
July 7 to 14, 2025
Rounds
Main Review, Remediation Review
Language
Solidity
Chains
Base, Arbitrum, Katana
Sector
Tokens
  • 0 Critical
  • 0 High
  • 2 Medium
  • 7 Low
  • 16 Informational

15 resolved · 10 acknowledged

Scope

Overview

Alongside engaged Guardian to review the security of their Universal Vault allowing users to deposit and earn yield from customized strategies. From the 7th of July to the 14th of July, a team of 5 auditors reviewed the source code in scope.

Findings 25

Main Review

17 findings
  1. M-01 Medium Incorrect maxMint Leads To Mint DoS Logical Error Resolved
    Location
    UniversalVault.sol: 427
    Round
    Main Review

    Description

    Currently function maxMint returns previewMint on the maxDeposit

    function maxMint() public view returns (uint256) {
         return previewMint(maxDeposit());
    }
    

    However there is an issue with that as previewMint is meant to accept shares and maxDeposit returns assets:

    function previewMint(uint256 _shares) public view returns (uint256) {
         UniversalVaultStorage storage $ = _getUniversalVaultStorage();
          return _convertToAssets(
               _shares, $.oracle.getLatestPrice(address(this)), Math.Rounding.Ceil
          );
    }
    

    This would mean that previewMint would convert our assets into assets (thinking they were shares) which would break the whole max assets/shares that anyone is able to deposit.

    Depending on the ratio between the two, this can either significantly decrease the cap for which mint can deposit up to or in the more dangerous scenario - significantly increase it.

    Recommendation

    Change previewMint to previewDeposit within maxMint.

    Resolution

    Alongside Team: The issue was resolved in commit 9f0ea52.

  2. M-02 Medium Incorrect Mint Slippage Protection Logical Error Resolved
    Location
    UniversalVault.sol: 229
    Round
    Main Review

    Description

    Function mint(uint256 _shares, address _receiver, uint256 _minAssets) takes a _minAssets parameter as a form of slippage protection:

    if (_minAssets = 0 && assets < _minAssets) {
        revert SlippageError(assets, _minAssets);
    }
    

    However, for proper slippage control, mint must revert if minting _shares costs more than a maxAssets of underlying tokens, rather than less than minAssets.

    This is because if the shares become more expensive, more assets may be required than the user expected.

    Recommendation

    Change function mint to use maxAssets instead of a _minAssets parameter, and update the inequality.

    Resolution

    Alongside Team: The issue was resolved in commit 7636b28.

  3. L-01 Low MAX_PRICE_CHANGE May Be Overly Restrictive Warning Resolved
    Location
    VaultOracle.sol
    Round
    Main Review

    Description

    Function _checkPriceChange restricts price changes to 20 bps per update interval (at least 1 hour).

    Although this may function normally for most strategies, if the manager is utilizing a strategy that handles external trading positions, a much larger price movement can occur in that time period that may not be appropriately delta neutral.

    Afterwards, the price change will be exceeded and the oracle updater will be unable to update the price, progress the epochs forward, and process redemptions.

    Recommendation

    Consider turning MAX_PRICE_CHANGE in a mutable variable that can be updated by the admin.

    Resolution

    Alongside Team: The issue was resolved in commit 79d1d417.

  4. L-02 Low New Manager Needs Prior Manager's Assets Warning Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    Appointing a new vault manager does not automatically transfer funds from the old manager, potentially leaving the new manager without assets to process withdrawals and trapping users' funds.

    The new manager would need control of the assets as well as ownership of any external positions.

    Recommendation

    Ensure all necessary assets are transferred to the new manager.

    Resolution

    Alongside Team: Acknowledged, this was already considered.

  5. L-03 Low Low Decimal Tokens Unsupported Warning Acknowledged
    Location
    VaultOracle.sol: 479
    Round
    Main Review

    Description

    Although the Universal Vault will be primarily used for universal assets, the Vault was designed to be token-agnostic.

    However, low decimal tokens may suffer excessive precision loss (e.g., 2 decimals like GUSD), such that oracle price updates are limited or entirely prevented.

    In the case of GUSD, maxDiff would floor to 0 and DoS Oracle/Queue operations: uint256 maxDiff = (lastPrice * MAX_PRICE_CHANGE) / 1e18;

    Recommendation

    Carefully select which assets will be used with the Vault and document this risk.

    Resolution

    Alongside Team: Acknowledged. We are going to use these vaults with uAssets (standard ERC20 tokens with 18 decimals) only. We are also considering USDC in the future, but not confirmed.

  6. L-04 Low Lack Of Minimum Deposit Warning Acknowledged
    Location
    UniversalVault.sol: 201, 229
    Round
    Main Review

    Description

    Currently there is a minimum withdrawal amount but not a minimum deposit amount. Non-malicious users typically do not deposit a couple wei of assets, and having that symmetrical validation would help ensure a user does not deposit and become stuck instantaneously.

    Recommendation

    Consider adding a minimum deposit amount of assets, or clearly document this behavior.

    Resolution

    Alongside Team: Acknowledged. This behavior is intended, we let users to deposit any amount as long as shares are not zero. Therefore, users have two options, wait until their invest have surpassed the threshold or deposit more uAssets, since the restriction to withdraw was implemented merely to prevent from spamming attacks in the WithdrawalQueue. This might be considered in the future.

  7. L-05 Low maxDeposit Rounds Up Rounding Resolved
    Location
    src/UniversalVault.sol
    Round
    Main Review

    Description

    maxDeposit is calculated as totalStakedLimit() - totalAssets(), where totalAssets() is calculated with FLOOR rounding. Because totalAssets is used to subtract from totalStakedLimit, but totalAssets is rounded down, this would increase the overall value of the maxDeposit.

    Recommendation

    Note that behaviour and set the limit accordingly, or roundup stakedAssets specifically within function maxDeposit().

    Resolution

    Alongside Team: The issue was resolved in commit e930413.

  8. L-06 Low Rebases In Queue Trap Funds Informational Acknowledged
    Location
    src/WithdrawalQueue.sol
    Round
    Main Review

    Description

    The vaults should be agnostic and be able to use any tokens, however if used with rebasing tokens the withdraw queue will experience rebases inside of it, as there would be time gaps between the manager calling completeFinalizeWithdraw and all users collecting their withdraws with claimWithdraw.

    During those time gaps rebases may occur, which would result in that rebase being bricked inside the contract.

    If negative rebases occur the manager may be required to separately send extra tokens in order to allow for all users to withdraw.

    Recommendation

    It's not recommended to use tokens such as stETH or any other rebasing tokens.

    Resolution

    Alongside Team: Acknowledged. We are going to use these vaults with uAssets (standard ERC20 tokens with 18 decimals) only. We are also considering USDC in the future, but not confirmed.

  9. I-01 Informational Modifier Never Used Best Practices Resolved
    Location
    WithdrawalQueue.sol: 123
    Round
    Main Review

    Description

    Modifier onlyVaultOwner is defined but never used within the WithdrawalQueue.

    Recommendation

    Consider removing the extraneous modifier definition.

    Resolution

    Alongside Team: The issue was resolved in commit de78e5d.

  10. I-02 Informational Zero Epoch Deposits Panic Documentation Resolved
    Location
    Global
    Round
    Main Review

    Description

    If a Vault is deployed without being activated in the VaultOracle in-tandem, user deposits will panic underflow when calling getLatestPrice since the epoch will be 0 for the vault:

    $.prices[vaultAddr][$.vaults[vaultAddr].epoch - 1];

    This may be an unexpected error and a more verbose custom error may be preferred.

    Recommendation

    Clearly document this or consider adding a more verbose error such as 'VaultNotActiveYet'.

    Resolution

    Alongside Team: The issue was resolved in commit 62836ff.

  11. I-03 Informational Trust Assumptions Documentation Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    Users of the Universal Vault system must trust a set privileged actors to act in good faith, including but not limited to:

    Manager Trust Assumptions:

    • Securely manages and protects user deposited assets.
    • Uses assets appropriately in yield-generating strategies.
    • Approves an allowance to the WithdrawalQueue and returns assets when users request

    withdrawals.

    • Requests withdraw finalization and completes batches in a timely, proper manner.

    Oracle Updater Trust Assumption:

    • Accurately prices in each epoch and handle price volatility.
    • Updates prices in a timely manner to ensure smooth withdrawal flow and without excess gas usage

    on claim.

    Vault Owner Trust Assumptions:

    • Pauses the Vault when necessary
    • Sets parameters such that the Vault operates safely, e.g. enforcing a large enough minWithdraw to

    prevent spam and likely malicious withdrawal requests.

    Recommendation

    Clearly document trust assumptions for privileged actors as well as the specs for the offchain system.

    Resolution

    Alongside Team: Acknowledged. Everything mentioned here will be properly documented prior to Vault's launch.

  12. I-04 Informational Unused VaultDeployed Event Best Practices Resolved
    Location
    VaultFactory.sol
    Round
    Main Review

    Description

    Although event VaultDeployed is defined it is not used which may negatively impact frontends relying on these emitted events.

    Recommendation

    Emit the event within function deployVault.

    Resolution

    Alongside Team: The issue was resolved in commit 12d0f04.

  13. I-05 Informational Staked Limit Passed Warning Acknowledged
    Location
    UniversalVault.sol
    Round
    Main Review

    Description

    The totalStakedLimit does not account for pending withdrawal assets, which are still held by the manager post-share burn but pre-finalization, allowing new deposits to exceed the effective limit and potentially overcommitting the strategy.

    Although this may be the intended behavior by the Vault Owner and Manager, it should be clearly documented.

    Recommendation

    Clearly document this behavior.

    Resolution

    Alongside Team: Acknowledged. As you mentioned, this is intended as we don't consider those pending withdrawals as regular positions since at some point they will be withdrawn but there will be a period in between where those “positions” will remain exposed to loses in the vault. We will documents this behavior more explicitly.

  14. I-06 Informational Pausable Initializer Not Called Best Practices Resolved
    Location
    UniversalVault.sol: 114
    Round
    Main Review

    Description

    Function __Pausable_init() is not called within the initialize function which goes against best practices to call __{ContractName}_init functions for all directly inherited contracts.

    Recommendation

    Consider adding __Pausable_init() in the initialize function.

    Resolution

    Alongside Team: The issue was resolved in commit 6b12a9a.

  15. I-07 Informational Unused Imports Best Practices Resolved
    Location
    Global
    Round
    Main Review

    Description

    • IERC20Metadata is imported within the UniversalVault but never used.
    • Math is imported within the Withdrawal Queue but never used.

    Recommendation

    Consider removing the unused import.

    Resolution

    Alongside Team: The issue was resolved in commit 3504d5d.

  16. I-08 Informational Owner Discrepancy Configuration Acknowledged
    Location
    VaultFactory.sol: 168
    Round
    Main Review

    Description

    When the factory initializes a UniversalVault and Withdrawal Queue, it sets the owner of these entities as the owner() of the Factory itself.

    However, if the Factory updates its owner via Ownable2Step, it doesn't update the owner for previously initialized Vaults and Withdrawal Queues.

    This creates a situation where outdated/incorrect owners exist for entities created via the factory.

    Recommendation

    Be aware of this scenario and update the owners of vaults and queues accordingly.

    Resolution

    Alongside Team: Acknowledged. We can update the owner by calling directly to the vaults we want to change their owner.

  17. I-09 Informational Contract Not 4626 Compliant Best Practices Acknowledged
    Location
    src/UniversalVault.sol
    Round
    Main Review

    Description

    Although the contracts are not intended to be fully compliant with ERC-4626, a few minor modifications could bring the vault closer to compliance:

    1. Rename the underlyingAsset function to asset().
    2. Ensure that the max functions (maxDeposit, maxMint, maxWithdraw, and maxRedeem) return 0

    when the contract is paused. Currently, deposit and withdraw operations revert as expected during a pause, meaning that users can technically deposit or withdraw 0 assets. However, the max functions return non-zero values, which creates an inconsistency.

    Recommendation

    Consider implementing those changes in order to make it easier for other projects to integrate.

    Resolution

    Alongside Team: We acknowledge this, we consider it will be almost impossible to be 100% compliant with ERC4626 due to the async withdrawal mechanism.

Remediation Review

8 findings
  1. L-01-R Low Accumulated Price Can Exceed Interval Logical Error Resolved
    Location
    VaultFactory.sol
    Round
    Remediation Review

    Description

    Function recalculateAccumulatedPrice had the upper bound validation changed from if(_upperBound > $.nextFinalizedWithdrawalId) revert InvalidUpperBound(); to if(_upperBound > $.nextWithdrawalId) revert InvalidUpperBound();

    Because the nextWithdrawalId can be much greater than the maximum id that has been requested for finalization, recalculateAccumulatedPrice will provide an inaccurate accumulation for a particular interval.

    Consider the following example:

    1. 5 requests to withdraw for 100e18 have been made and the withdrawal ids are as following: [0, 1, 2, 3, 4, 5]
    2. Manager calls requestFinalizeWithdraw with _upToWithdrawalId = 0
    3. nextFinalizationRequestId is now 1
    4. Off-chain script triggers recalculateAccumulatedPrice and passes upper bound with id 4
    5. Accumulated amount (assuming price of 1e18) is 100e18 * 5 rather than 100e18
    6. During claims too much will be withdrawn by the user and consequent users will experience ERC20InsufficientBalance reverts.

    Recommendation

    Be extremely careful with the inputs from the off-chain scripts, or update the validation accordingly.

    Resolution

    Alongside Team: The issue was resolved in PR#19. 31

  2. I-01-R Informational Suffix Typo Informational Resolved
    Location
    VaultFactory.sol: 26-27
    Round
    Remediation Review

    Description

    NAME_SUFIX and SYMBOL_SUFIX both have a typo in the variable names. It should be NAME_SUFFIX and SYMBOL_SUFFIX respectively.

    Recommendation

    Update the variable naming.

    Resolution

    Alongside Team: The issue was resolved in commit acd7a89.

  3. I-02-R Informational Visibility Conventions Informational Resolved
    Location
    VaultFactory.sol: 160
    Round
    Remediation Review

    Description

    The underscore that prefixes the function name in the _getBytecode function indicates that the function will be either internal or private. However, the function is public.

    Recommendation

    Remove the underscore to follow the same visibility conventions used elsewhere in the contract.

    Resolution

    Alongside Team: The issue was resolved in commit 3ba7d0b.

  4. I-03-R Informational Incorrect Natspec Informational Resolved
    Location
    VaultOracle.sol: 273
    Round
    Remediation Review

    Description

    The NatSpec comment for the registerNewVault function indicates that the function is internal. However, the function is actually external.

    Recommendation

    Update the comment to indicate that the function is external.

    Resolution

    Alongside Team: The issue was resolved in commit 9ffdf75.

  5. I-04-R Informational Unreachable Code Informational Resolved
    Location
    WithdrawalQueue.sol: 604-609
    Round
    Remediation Review

    Description

    The code below the binary search in _findCheckpointEpoch can not be hit. After many fuzzing runs, this section of code had not achieved execution.

    Recommendation

    Consider removing the code if verified to be unreachable.

    Resolution

    Alongside Team: The issue was resolved in commit 1b326e7.

  6. I-05-R Informational No Two Vaults Can Have Same Name And Symbol Documentation Acknowledged
    Location
    VaultFactory.sol: 79
    Round
    Remediation Review

    Description

    Function deployVault deploys the Vault and WithdrawalQueue with salts based on the _name and _symbol, hence any attempted deployment with the same name and symbol and on the same chain would lead to a CREATE2 collision.

    This is not an issue since only the owner can utilize the factory and existing deployments can be upgraded, but should be clearly communicated internally.

    Recommendation

    Be aware of this behavior.

    Resolution

    Alongside Team: Acknowledged.

  7. I-06-R Informational Redundant Activity Check Documentation Resolved
    Location
    VaultOracle.sol: 336
    Round
    Remediation Review

    Description

    The validation if ($.vaults[_vaultAddr].active = false) revert VaultNotActive(); was added to function deactivateVault, but it already has modifier onlyActiveVault.

    Recommendation

    Remove the redundant validation.

    Resolution

    Alongside Team: The issue was resolved in commit 8285c91.

  8. I-07-R Informational Vault Pause And Activation Asymmetry Documentation Acknowledged
    Location
    Global
    Round
    Remediation Review

    Description

    When a vault is deactivated within the Vault Oracle, price updates, finalization requests, and completions are prevented.

    When a vault is paused, users cannot deposit nor initiate withdrawals from the vault, cannot request, withdraw nor claim from the queue, but prices can continue to be updated and epochs can advance.

    Because there is more than one way to prevent the same functionality with slight differences, it should be clearly defined when the vault is expected to be paused and when it is expected to be deactivated through the oracle.

    Recommendation

    Clearly document this behavior.

    Resolution

    Alongside Team: Acknowledged. We will document this properly in the front-end.

Invariants 24

The review's fuzzing suite asserted 24 invariants. 23 held and 1 did not.

Every invariant tested
IDInvariantResult
GLOB-01Vault’s maxDeposit() ≈ previewMint(maxMint())Broken
GLOB-02Vault’s totalAssets() = convertToAssets(totalSupply())Held
GLOB-03Vault’s maxMint() = previewDeposit(maxDeposit())Held
GLOB-04maxWithdraw(this) < totalAssets()Held
GLOB-05convertToAssets(maxMint()) < maxDeposit()Held
GLOB-06nextFinalizedWithdrawalId() < nextWithdrawalId()Held
GLOB-07No gaps in completed finalization requestsHeld
GLOB-08nextCompletedWithdrawId() < nextFinalizedWithdrawalId()Held
GLOB-09Every request’s checkpointPtr validHeld
GLOB-10Last checkpoint upperBound +1 = nextCompletedWithdrawId()Held
GLOB-11Every request’s checkpointPtr < nextCheckpointIdHeld
GLOB-12NFTs exist for non-completed withdrawal IDsHeld
GLOB-13Active observations length = finalize requests - completesHeld
GLOB-14Active list points to valid observationsHeld
GLOB-15Checkpoints have monotonically increasing contiguous boundsHeld
GLOB-16Binary search matches linear search resultsHeld
DEP-01totalAssets() < totalStakedLimit()Held
DEP-02Post-deposit totalAssets < pre + assets (approx eq)Held
DEP-03Post-deposit maxDeposit > pre - assets (approx)Held
MNT-01totalAssets() < totalStakedLimit()Held
MNT-02Post-mint totalSupply = pre + sharesHeld
MNT-03Post-mint maxMint > pre - shares (approx)Held
REQW-01Current epoch price = 0 post-request withdrawalHeld
CMPLT-01IntervalKey not active after completionHeld

More from Universal

  1. Hook Contracts

    61 findings 61 findings: 5 medium, 15 low, 41 informational

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote