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
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-
M-01 Medium Incorrect maxMint Leads To Mint DoS Logical Error Resolved
Description
Currently function
maxMintreturnspreviewMinton themaxDepositfunction maxMint() public view returns (uint256) { return previewMint(maxDeposit()); }However there is an issue with that as
previewMintis meant to accept shares andmaxDepositreturns 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
previewMintwould 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
mintcan deposit up to or in the more dangerous scenario - significantly increase it.Recommendation
Change
previewMinttopreviewDepositwithinmaxMint.Resolution
Alongside Team: The issue was resolved in commit 9f0ea52.
-
M-02 Medium Incorrect Mint Slippage Protection Logical Error Resolved
Description
Function
mint(uint256 _shares, address _receiver, uint256 _minAssets)takes a_minAssetsparameter as a form of slippage protection:if (_minAssets = 0 && assets < _minAssets) { revert SlippageError(assets, _minAssets); }However, for proper slippage control,
mintmust revert if minting_sharescosts more than amaxAssetsof underlying tokens, rather than less thanminAssets.This is because if the shares become more expensive, more assets may be required than the user expected.
Recommendation
Change function
mintto usemaxAssetsinstead of a_minAssetsparameter, and update the inequality.Resolution
Alongside Team: The issue was resolved in commit 7636b28.
-
L-01 Low MAX_PRICE_CHANGE May Be Overly Restrictive Warning Resolved
Description
Function
_checkPriceChangerestricts 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_CHANGEin a mutable variable that can be updated by the admin.Resolution
Alongside Team: The issue was resolved in commit 79d1d417.
-
L-02 Low New Manager Needs Prior Manager's Assets Warning Acknowledged
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.
-
L-03 Low Low Decimal Tokens Unsupported Warning Acknowledged
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,
maxDiffwould 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.
-
L-04 Low Lack Of Minimum Deposit Warning Acknowledged
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.
-
L-05 Low maxDeposit Rounds Up Rounding Resolved
Description
maxDepositis calculated astotalStakedLimit() - totalAssets(), wheretotalAssets()is calculated with FLOOR rounding. BecausetotalAssetsis used to subtract fromtotalStakedLimit, buttotalAssetsis rounded down, this would increase the overall value of themaxDeposit.Recommendation
Note that behaviour and set the limit accordingly, or roundup
stakedAssetsspecifically within functionmaxDeposit().Resolution
Alongside Team: The issue was resolved in commit e930413.
-
L-06 Low Rebases In Queue Trap Funds Informational Acknowledged
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
completeFinalizeWithdrawand all users collecting their withdraws withclaimWithdraw.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
stETHor 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.
-
I-01 Informational Modifier Never Used Best Practices Resolved
Description
Modifier
onlyVaultOwneris defined but never used within theWithdrawalQueue.Recommendation
Consider removing the extraneous modifier definition.
Resolution
Alongside Team: The issue was resolved in commit de78e5d.
-
I-02 Informational Zero Epoch Deposits Panic Documentation Resolved
Description
If a Vault is deployed without being activated in the
VaultOraclein-tandem, user deposits will panic underflow when callinggetLatestPricesince 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.
-
I-03 Informational Trust Assumptions Documentation Acknowledged
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
WithdrawalQueueand 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
minWithdrawto
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.
-
I-04 Informational Unused VaultDeployed Event Best Practices Resolved
Description
Although
event VaultDeployedis 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.
-
I-05 Informational Staked Limit Passed Warning Acknowledged
Description
The
totalStakedLimitdoes 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.
-
I-06 Informational Pausable Initializer Not Called Best Practices Resolved
Description
Function
__Pausable_init()is not called within theinitializefunction which goes against best practices to call__{ContractName}_initfunctions for all directly inherited contracts.Recommendation
Consider adding
__Pausable_init()in theinitializefunction.Resolution
Alongside Team: The issue was resolved in commit 6b12a9a.
-
I-07 Informational Unused Imports Best Practices Resolved
Description
IERC20Metadatais imported within theUniversalVaultbut never used.Mathis imported within the Withdrawal Queue but never used.
Recommendation
Consider removing the unused import.
Resolution
Alongside Team: The issue was resolved in commit 3504d5d.
-
I-08 Informational Owner Discrepancy Configuration Acknowledged
Description
When the factory initializes a
UniversalVaultand Withdrawal Queue, it sets the owner of these entities as theowner()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.
-
I-09 Informational Contract Not 4626 Compliant Best Practices Acknowledged
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:- Rename the
underlyingAssetfunction toasset(). - Ensure that the max functions (
maxDeposit,maxMint,maxWithdraw, andmaxRedeem) 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.
- Rename the
Remediation Review
8 findings-
L-01-R Low Accumulated Price Can Exceed Interval Logical Error Resolved
Description
Function
recalculateAccumulatedPricehad the upper bound validation changed fromif(_upperBound > $.nextFinalizedWithdrawalId) revert InvalidUpperBound();toif(_upperBound >$.nextWithdrawalId) revert InvalidUpperBound();Because the
nextWithdrawalIdcan be much greater than the maximum id that has been requested for finalization,recalculateAccumulatedPricewill provide an inaccurate accumulation for a particular interval.Consider the following example:
- 5 requests to withdraw for 100e18 have been made and the withdrawal ids are as following: [0, 1, 2, 3, 4, 5]
- Manager calls
requestFinalizeWithdrawwith_upToWithdrawalId = 0 nextFinalizationRequestIdis now 1- Off-chain script triggers
recalculateAccumulatedPriceand passes upper bound with id 4 - Accumulated amount (assuming price of 1e18) is
100e18 * 5rather than100e18 - During claims too much will be withdrawn by the user and consequent users will experience
ERC20InsufficientBalancereverts.
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
-
I-01-R Informational Suffix Typo Informational Resolved
Description
NAME_SUFIXandSYMBOL_SUFIXboth have a typo in the variable names. It should beNAME_SUFFIXandSYMBOL_SUFFIXrespectively.Recommendation
Update the variable naming.
Resolution
Alongside Team: The issue was resolved in commit acd7a89.
-
I-02-R Informational Visibility Conventions Informational Resolved
Description
The underscore that prefixes the function name in the
_getBytecodefunction 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.
-
I-03-R Informational Incorrect Natspec Informational Resolved
Description
The
NatSpeccomment for theregisterNewVaultfunction indicates that the function isinternal. 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.
-
I-04-R Informational Unreachable Code Informational Resolved
Description
The code below the binary search in
_findCheckpointEpochcan 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.
-
I-05-R Informational No Two Vaults Can Have Same Name And Symbol Documentation Acknowledged
Description
Function
deployVaultdeploys the Vault andWithdrawalQueuewith salts based on the_nameand_symbol, hence any attempted deployment with the same name and symbol and on the same chain would lead to aCREATE2collision.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.
-
I-06-R Informational Redundant Activity Check Documentation Resolved
Description
The validation
if ($.vaults[_vaultAddr].active = false) revert VaultNotActive();was added to functiondeactivateVault, but it already has modifieronlyActiveVault.Recommendation
Remove the redundant validation.
Resolution
Alongside Team: The issue was resolved in commit 8285c91.
-
I-07-R Informational Vault Pause And Activation Asymmetry Documentation Acknowledged
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.
No findings match.
Invariants 24
The review's fuzzing suite asserted 24 invariants. 23 held and 1 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
GLOB-01 | Vault’s maxDeposit() ≈ previewMint(maxMint()) | Broken |
GLOB-02 | Vault’s totalAssets() = convertToAssets(totalSupply()) | Held |
GLOB-03 | Vault’s maxMint() = previewDeposit(maxDeposit()) | Held |
GLOB-04 | maxWithdraw(this) < totalAssets() | Held |
GLOB-05 | convertToAssets(maxMint()) < maxDeposit() | Held |
GLOB-06 | nextFinalizedWithdrawalId() < nextWithdrawalId() | Held |
GLOB-07 | No gaps in completed finalization requests | Held |
GLOB-08 | nextCompletedWithdrawId() < nextFinalizedWithdrawalId() | Held |
GLOB-09 | Every request’s checkpointPtr valid | Held |
GLOB-10 | Last checkpoint upperBound +1 = nextCompletedWithdrawId() | Held |
GLOB-11 | Every request’s checkpointPtr < nextCheckpointId | Held |
GLOB-12 | NFTs exist for non-completed withdrawal IDs | Held |
GLOB-13 | Active observations length = finalize requests - completes | Held |
GLOB-14 | Active list points to valid observations | Held |
GLOB-15 | Checkpoints have monotonically increasing contiguous bounds | Held |
GLOB-16 | Binary search matches linear search results | Held |
DEP-01 | totalAssets() < totalStakedLimit() | Held |
DEP-02 | Post-deposit totalAssets < pre + assets (approx eq) | Held |
DEP-03 | Post-deposit maxDeposit > pre - assets (approx) | Held |
MNT-01 | totalAssets() < totalStakedLimit() | Held |
MNT-02 | Post-mint totalSupply = pre + shares | Held |
MNT-03 | Post-mint maxMint > pre - shares (approx) | Held |
REQW-01 | Current epoch price = 0 post-request withdrawal | Held |
CMPLT-01 | IntervalKey not active after completion | Held |
More from Universal
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.
