Hyperfolio Info
Hyperfolio, an automated asset manager re-defining yield generation on blockchain.
TrustNet Score
The TrustNet Score evaluates crypto projects based on audit results, security, KYC verification, and social media presence. This score offers a quick, transparent view of a project's credibility, helping users make informed decisions in the Web3 space.
Real-Time Threat Detection
Real-time threat detection, powered by Cyvers.io,
is currently not
activated
for this project.
This advanced feature provides continuous monitoring and instant alerts to safeguard your assets from potential security threats. Real-time detection enhances your project's security by proactively identifying and mitigating risks.
For more information, click here.
Security Assessments
Summary and Final Words
No crucial issues found
The contract does not contain issues of high or medium criticality. This means that no known vulnerabilities were found in the source code.
Contract owner cannot mint
It is not possible to mint new tokens.
Contract owner cannot blacklist addresses.
It is not possible to lock user funds by blacklisting addresses.
Contract owner cannot set high fees
The fees, if applicable, can be a maximum of 25% or lower. The contract can therefore not be locked. Please take a look in the comment section for more details.
Contract cannot be locked
Owner cannot lock any user funds.
Token cannot be burned
There is no burning within the contract without any allowances
Ownership is not renounced
The owner retains significant control, which could potentially be used to modify key contract parameters.
Contract is upgradeable
The contract uses a proxy pattern or similar mechanism, enabling future upgrades. This can introduce risks if the upgrade mechanism is not securely managed.
Scope of Work
This audit encompasses the evaluation of the files listed below, each verified with a SHA-1 Hash. The team referenced above has provided the necessary files for assessment.
The auditing process consists of the following systematic steps:
- Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
- Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
- Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
- Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
- Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
- Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
- Actionable Recommendations: Provide detailed, specific, and actionable steps to secure and optimize the smart contracts.
A file with a different Hash has been intentionally or otherwise modified after the security review. A different Hash may indicate a changed condition or potential vulnerability that was not within the scope of this review.
Final Words
The following provides a concise summary of the audit report, accompanied by insightful comments from the auditor. This overview captures the key findings and observations, offering valuable context and clarity.
Smart Contract Analysis Statement - Re-Audit, Third Round
Contract Analysis
HyperfolioVault is an upgradeable custody vault. It accepts one configured asset, either an ERC20 token or the chain's native currency, and forwards it to an interchangeable external strategy expected to generate yield. It is not a token: no supply, no transfers, no mint and no burn. Deposits and withdrawals are not open to the public and can only be initiated by one designated gateway contract. The vault keeps an internal ledger of what each user deposited and pays withdrawals against it.
This is the third review. The revision examined here is small, one file and some thirty lines, and it closes the two defects the previous report asked for first. Withdrawals are now served from the vault's own liquidity before the strategy is touched, and the vault has become the single point from which users are paid, so a depositor always receives exactly the amount recorded against their position no matter how the liquidity was split. The check that verifies delivery became a lower bound rather than an exact match, so a strategy returning marginally more than requested no longer freezes a position permanently. The reward channel gained a check that stops a strategy handing depositor principal to the owner. Earlier rounds had already closed the accounting defect that made the vault insolvent with any asset that taxes transfers, made strategy migration all-or-nothing, and added an emergency hatch that detaches a broken strategy without calling into it.
After this revision no high severity and no medium severity issue remains open. Across the engagement we record seventeen issues resolved and nine accepted as documented design decisions, with ten low severity issues, twelve informational observations and seven gas optimisations still open. We found no critical issues, in the sense that no unprivileged party can drain the contract. The open items concern protocol revenue and unsupported assets rather than depositor funds: the reward channel is bounded too tightly to move the yield it was built for, surplus retained in the vault has no route out, and a withdrawal the vault covers on its own is paid without a delivery check.
Verification ran against the compiled contract rather than the source alone, using static analysis, pattern scanning, symbolic execution and three independent fuzzing engines. One hundred and three test cases cover the contract, alongside twelve invariant properties and sixteen symbolic ones. Of the eighty-one distinct cases preserved from the two earlier rounds, seventy-five were unaffected and six changed status, every one in the intended direction, which is the evidence that this revision caused no regression. No state variable was added, removed or reordered, so the storage layout is identical to the one previously reviewed and the upgrade is safe.
Ownership Privileges
Ownership is not yet established, because the contract was not deployed at the time of review; initialisation assigns it to whichever address performs it. The owner can:
- Replace the yield strategy at any time, with immediate effect. This is by far the most consequential power in the contract: attaching a strategy grants it an unlimited allowance over the vault's asset and pushes the entire balance to it. The incoming strategy must be a deployed contract declaring the same asset, which stops a mistyped address but is no barrier to a deliberately hostile one. There is no timelock, and the deposit pause does not cover this path, so it stays available during an incident. That absence is a deliberate decision by the team, discussed under risks below.
- Claim strategy rewards to an address of their choosing. This is no longer a route to depositor principal: the vault rejects the claim if the strategy's reported balance falls across it.
- Rescue tokens other than the configured asset, and native currency from a token-mode vault. The configured asset can never be swept, so there is no route from these functions to user deposits, which we verified.
- Change the gateway contract permitted to deposit and withdraw. A zero address is rejected.
- Change the configured asset and the sentinel that selects token or native mode, but only with the strategy detached and only while the internal ledger reads zero.
- Appoint or remove a guardian and lift a deposit pause. The guardian can pause deposits but cannot lift the pause and cannot affect withdrawals in any way.
- Transfer ownership, which requires the incoming owner to accept. Renouncing ownership is disabled and reverts unconditionally, so the owner-only recovery paths cannot be orphaned.
The owner cannot alter any user's recorded balance, credit or delete a ledger entry directly, block or freeze an address, charge a fee, or halt withdrawals. There is no mint, no burn, no blacklist and no fee parameter anywhere in the contract. We confirmed by test and symbolically that no combination of pause state and caller can stop a user exiting.
Security Features
- The deposit and withdrawal paths are protected against reentrancy, as are the strategy setter, the emergency detach and the reward claim.
- Deposits are verified rather than inferred. The vault measures its own balance across the transfer and requires the delta to equal the requested amount, so nothing can be credited that it did not receive in full.
- Withdrawals are paid by the vault itself and draw on the strategy only for the shortfall, so assets the vault holds are always reachable and the depositor is insulated from how the strategy routes funds.
- Strategy migration is all-or-nothing: funds go through the strategy's own deposit entry point and must be registered, or the whole change reverts. An emergency detach removes a broken strategy without calling into it at all, not even to read a balance.
- Ownership is two-step and cannot be renounced, confirmed symbolically over all callers. The upgradeable initialisation pattern is correct and the implementation contract cannot be claimed by an outside party.
- Access to user funds is narrowed to a single gateway address rather than being open to the public.
- The attack surface behind most vault losses is simply absent. There is no price oracle to manipulate, no swap or liquidity path to sandwich, no signature to replay, no unbounded loop and no cross-chain messaging. Arithmetic is checked throughout, with no unchecked blocks anywhere.
Principal Risks for the Project Team and for Investors
- Administrative power is unconstrained, and the team has decided it should stay that way. This is the largest single gap between the contract's technical quality and its readiness to hold user funds. We verified end to end that an owner can attach a hostile strategy, move the whole balance in one transaction, block every withdrawal, and sweep later inflows through the standing allowance. We recommended a timelock on the privileged setters and the team declined it deliberately, on the grounds that a mandatory delay would equally prevent the protocol from reacting at speed to a security incident, which is what keeping these controls immediate is meant to allow. We consider that a legitimate trade-off and record it as an accepted design decision rather than an unresolved defect, with one qualification: the exposure comes almost entirely from the single function that attaches a strategy, while the emergency responses the argument protects are separate functions that could stay immediate either way. None of this is a coding defect. It does mean users are trusting a single key directly, with no notice period and no opportunity to exit ahead of a change. Independently of the timelock decision we would still expect ownership held by a published multisig, the strategy allowance sized to the deposit rather than left unlimited, and the reward claim bounded correctly; none of those three slows an emergency response down.
- Loose ends now sit on protocol revenue rather than on depositor funds. None of the remaining open issues can cost a depositor their principal. The channel built to collect strategy yield currently rejects claims that move only yield, because it is bounded against the strategy's balance before the claim rather than against what the vault owes its users. Surplus reaching the vault when a strategy returns slightly more than requested is retained by design but has no route out. And the check on what a depositor received applies only when the strategy is entered, so a withdrawal the vault covers itself is unverified, which matters for an asset that shortens transfers. All three are small, well-understood changes and should be made before the vault holds funds, so that the accounting the project reports matches what the contract does.
- The repository still cannot build itself, for the third round running. It contains only the four source files, with no build configuration, no lockfile and no deployment scripts, so the build again relied on an auditor-supplied configuration and auditor-pinned dependencies. The compiler version, optimizer settings, target chain rules and library versions that will produce the deployed bytecode are therefore unverifiable from the repository. That matters more than usual here, because the reentrancy guard depends on transient storage and so on a Cancun-capable chain, a requirement currently recorded only in a source comment. Committing the build configuration and pinning every pragma would remove the possibility of the audited and deployed bytecode diverging.
On the pre-defined claims commonly checked by investors, our verdict is as follows. The contract cannot mint, cannot burn, cannot blacklist and cannot set fees, because none of those mechanisms exist in it. It is upgradeable, and the upgrade authority sits entirely outside the audited code with no delay attached, so the implementation should be assumed replaceable at any time by whoever controls the proxy administrator; that address should be published, held separately from the vault owner, and placed behind a timelock. On whether funds can be locked, the answer is now no for every scenario we were able to construct: ownership cannot be renounced, a broken strategy can always be detached, and the two temporary lock-ups that survived the previous revision are closed by this one. The only case we found in which recorded principal cannot be withdrawn requires the vault to be configured with an asset the project already documents as unsupported. On a delay for the privileged setters, the team has chosen not to add one so that it can respond to incidents immediately; investors should read that as meaning the owner key can replace the strategy and move the entire balance at any moment, without notice.
Note - This Audit report consists of a security analysis of the HyperfolioVault smart contract. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the HyperfolioVault team. Other contracts associated with the project were not audited by our team, and this matters more than usual here: the gateway contract that controls all access to the vault, and every strategy contract the vault will delegate funds to, are both fully trusted by the code we reviewed and both fall outside this scope. That trust narrowed slightly with this revision, because the vault no longer relies on the strategy to pay the depositor correctly, but it still relies on strategies to register migrated funds and to report their own balance honestly, and neither property can be verified from inside the vault. The decision that yield belongs to the protocol also means depositors receive principal only, so the economics of the vault depend entirely on the farm layer paying out, and that layer is likewise outside this scope. We recommend investors do their own research before investing.
Files and details
Findings and Audit result
high Issues | 4 findings
Resolved
#1 high Issue
Fee-on-transfer accounting covers only the first transfer hop, leaving the vault insolvent
The deposit path reads the vault's reported balance before and after pulling tokens from the gateway and credits the depositor with the difference. That measurement captures the fee taken on the first transfer only. Immediately afterwards the vault instructs the strategy to pull the same amount out again, and because the strategy holds an unlimited allowance that second transfer is taxed a second time. The shortfall is never measured or recorded, so the per-user ledger and the recorded total both overstate what the vault and its strategy actually hold. The gap grows with every deposit, which means the earliest withdrawers are paid in full while later ones are left holding entries they cannot redeem. Any asset that delivers less than the requested amount is affected, including deflationary and rebasing designs. This was reproduced with a one percent fee token, where a deposit of 1,000 credits the user with 990 while only 980.1 reaches the strategy, and the attempt to withdraw the credited amount reverts.
Resolved
#2 high Issue
Withdrawals ignore assets held by the vault, so a solvent vault can fail to pay
When a strategy is attached, a withdrawal always requests the full amount from the strategy and pays the user out of whatever the strategy sends. Assets sitting in the vault itself are never used, even though the public balance accessor counts them towards the amount a user is permitted to request. A strategy that returns less than asked, or that caps how much can leave in one call, therefore causes the entire withdrawal to fail while the vault holds more than enough to cover it. The same mismatch means a partially funded strategy blocks users who could otherwise be paid immediately, and the deposit and withdrawal paths disagree about what counts as available.
Resolved
#3 high Issue
Strategy migration pushes funds without registering them, stranding the transferred amount
Attaching a new strategy transfers the vault's entire holdings to that strategy with a plain token or native transfer. The strategy is never told that a deposit took place, so a strategy that tracks its principal internally records nothing and reports a zero balance while physically holding the funds. Withdrawals then fail because the strategy believes it holds nothing to return, and the vault has already emptied itself. The transfer is also unverified, so if it moves nothing the vault carries on as though migration had succeeded. Nothing in the flow requires the receiving strategy to acknowledge the transfer, which makes the stranding silent. The worst case is theft rather than stranding. Because the funds sit in the strategy as an unregistered raw balance, they are exposed to whatever recovers stray tokens on that contract. Where a strategy exposes a rescue function that anyone can call, the whole migrated balance can be taken by an unprivileged address while the vault's ledger still shows the full liability. That pattern is normally restricted to the strategy owner, in which case the funds stay stranded but are not stolen, so the exposure depends on a component outside the scope of this review.
Resolved
#4 high Issue
A halted strategy freezes every withdrawal and cannot be detached
Detaching or replacing a strategy begins by calling that strategy to withdraw everything. If the call reverts for any reason, whether a paused strategy, an exhausted external protocol or a dependency that no longer exists, the configuration change reverts with it and the broken strategy stays attached. Because withdrawals are also routed exclusively through the strategy, users cannot exit either. There is no escape hatch: no pause, no forced detach and no path that pays users from the vault's own holdings. Every deposit stays trapped for as long as the strategy remains unhealthy, and because ownership can be renounced the freeze can become permanent.
medium Issues | 9 findings
Resolved
#1 medium Issue
Changing the asset or the native sentinel leaves stale unlimited approvals
The asset and the native currency sentinel can each be changed while a strategy is attached. Neither setter revokes the unlimited allowance granted under the previous configuration, and neither grants an allowance for the new one. The outcome is a strategy that still holds an unlimited approval over a token the vault no longer uses, together with no approval at all over the token it now does use, so deposits fail until a strategy is attached again. Changing the sentinel can also flip the vault between token mode and native mode without touching the strategy, leaving the two out of step in a way neither can detect. The guard on both setters checks the recorded total rather than the real balance, so a vault holding stray funds still passes it.
Resolved
#2 medium Issue
Deposit size is derived from a balance that includes the strategy's self-reported figure
The deposit path sizes the credit from the difference between two readings of a combined figure that adds the vault's own holdings to whatever the strategy reports. Any movement in the strategy's reported balance during the token transfer is therefore attributed to the depositor. An asset with a transfer hook, or a strategy whose reported balance moves for an unrelated reason, lets a depositor capture value they never supplied, and a figure that moves downwards makes the subtraction revert and blocks deposits entirely. The strategy's report is accepted without any bound or sanity check even though it is a single external source that the vault does not control. The severity of this depends on the asset. A periodic or oracle-driven rebase cannot be captured this way, because it does not occur during the transfer, and neither can yield already sitting in the pool or a harvest performed in the pre-deposit hook, which is snapshotted beforehand. A reflection-on-transfer asset is capturable but the amount is bounded by the reflection on that single transfer. An asset that hands control to third-party code during the transfer is capturable without any bound at all, and was measured crediting 5,001 for a single token deposited. The rating reflects the fact that choosing such an asset is an owner decision rather than something an attacker selects, and it should be treated as high if any hooked, reflection or transfer-rebasing asset enters the intended asset set.
Resolved
#3 medium Issue
Native currency sent to a token-mode vault is permanently locked
The deposit entry point is payable and the contract exposes an open receive function, but neither checks the vault's operating mode and neither reconciles the value sent against the amount requested. Native currency arriving while the vault is configured for a token is recorded nowhere and cannot be withdrawn, because the withdrawal path only ever moves the configured asset. There is no rescue function, so the funds are lost outright. The open receive function also accepts value from any sender rather than only from the strategy returning funds, which widens the surface for accidental loss.
Resolved
#4 medium Issue
The strategy's asset is never validated against the vault's asset
A strategy is attached with no check that it is even a contract and no check that the asset it declares matches the asset the vault holds. The strategy interface exposes an asset accessor for precisely this purpose and it is never called anywhere in the vault. Attaching a mismatched or codeless strategy still succeeds, still grants an unlimited allowance and still pushes the vault's entire balance out. In the codeless case the push reports success while the funds go somewhere they cannot be retrieved from, so a single mistyped address is enough to lose everything the vault holds.
Resolved
#5 medium Issue
Withdrawals never verify that the strategy actually paid the recipient
The withdrawal path reduces the user's recorded balance and the recorded total by the requested amount, hands the payment off to the strategy, and returns the requested figure to the caller. It never establishes any post-condition on that hand-off. A strategy that transfers less than asked, or nothing at all, satisfies the call as long as it does not revert, and the user's entire claim is cleared against it with no error and no signal to the gateway. This was reproduced with an ordinary token that charges no fee whatsoever: a strategy retaining five percent left the user with 950 while the vault reported 1,000 withdrawn, and a strategy whose withdrawal is a no-op erased the claim entirely while reporting success. The value stays inside the strategy. The same gap exists on the native path. Note that where the asset itself charges a transfer fee, the shortfall the recipient sees is properly borne by the withdrawing user and is not a defect - the vault did release the full requested amount.
Acknowledged
#6 medium Issue
Strategy yield is never accounted for and cannot reach users
The vault records deposits as principal and pays withdrawals against that principal. Any profit the strategy earns raises the balance the vault reports but never raises any user's entitlement, so it accumulates in a pool that nobody can claim. The strategy interface declares functions for harvesting and claiming rewards and the vault calls neither. Users receive exactly what they put in, which contradicts the stated purpose of delegating funds to a strategy for yield, and the unassigned surplus becomes a standing incentive for whoever is able to reconfigure the vault.
Resolved
#7 medium Issue
No pause, no emergency withdrawal and no circuit breaker
The vault holds all deposited value, routes everything through a single external strategy, and offers no way to stop accepting deposits while an incident is being investigated. There is no guardian role, no emergency path that bypasses the strategy and no rate limiting on any privileged action. When something goes wrong the only available response is to change configuration, and the functions that do so themselves depend on the strategy being healthy, which is exactly the condition that will not hold during an incident.
Resolved
#8 medium Issue
Withdrawal delivery check demands an exact match, so a strategy that returns more than requested blocks the exit
The new delivery post-condition on both withdrawal paths requires the recipient's balance to have risen by exactly the requested amount. Under-delivery is correctly rejected, but so is over-delivery, and over-delivery is the ordinary outcome of unwinding a position that is held as integer shares in an external market: redeeming shares for a target amount routinely returns a little more, and a strategy that forwards what it actually received will therefore always fail the check. The same is true of a strategy that hands back principal together with a small accrued increment. Because the ledger debit happens before the strategy call and the whole transaction reverts, no funds are lost, but the position cannot be exited at all for as long as the strategy behaves that way, and the vault gives the owner no way to relax the condition. On the native path the check has a second consequence: it measures the recipient's balance rather than the amount transferred, so a smart-contract wallet that forwards incoming value inside its receive hook always shows a delta of zero and can never be paid, and a recipient that spends more than it received makes the subtraction underflow into a panic rather than the intended revert message. Reproduced with a strategy that returns one wei more than asked, which is enough to freeze a one thousand token position, and with a forwarding wallet, which can never withdraw at all. Two independent static analysers flag the same three lines as dangerous strict equality on a balance.
Acknowledged
#9 medium Issue
The owner can seize every deposit in a single transaction
Setting a new strategy is unrestricted, takes effect in the same transaction and grants the target an unlimited allowance over the vault's asset before pushing the entire balance to it. An owner, or anyone who obtains the owner key, can point the vault at a contract they control and take all user funds at once. There is no delay, no confirmation step, no validation that the target behaves like a strategy and no cap on the amount at risk. Ownership is a single address that can also be renounced, and the gateway contract that fronts the vault is trusted without any conditions attached. This is the single largest risk in the contract and it exists by design rather than by mistake, so it needs to be addressed through governance controls rather than a code fix alone.
low Issues | 18 findings
Acknowledged
#1 low Issue
The transfer helper treats calls into addresses with no code as successful transfers
The helper performs a low level call and accepts the outcome whenever the call succeeded and either returned nothing or returned a truthy value. A call into an address with no code always succeeds and returns nothing, so a transfer to a non existent token is reported as a success. The vault reaches this path when it is configured with an asset that is not a contract, or when a mode change points it at an address that never held code, and it will then update its ledger against a transfer that never happened. The helper is generic, so any future caller inherits the same blind spot.
Resolved
#2 low Issue
Ownership can be renounced and ownership transfer is single-step
The contract inherits an ownership module that lets the owner renounce ownership outright and transfer it to any address in one step. Renouncing is unusually damaging here because several recovery paths, including detaching a failed strategy, are owner only. A renounced vault with a broken strategy is permanently frozen with user funds inside it and nobody able to act. A single step transfer also allows ownership to be handed to a mistyped or uncontrolled address with no way to reverse the mistake.
Acknowledged
#3 low Issue
Amount parameters are unvalidated on both the deposit and the withdrawal path
In native mode the credited amount is taken from the value attached to the call while the amount parameter is ignored entirely, so a caller can pass one figure and send another without anything objecting. Neither entry point rejects a zero amount either. A zero-amount withdrawal is particularly odd: the balance check passes trivially for any address, so the call succeeds for someone who has never deposited, emits a withdrawal event and reaches into the strategy with a zero amount. On the handful of tokens that reject zero-value transfers the call reverts instead, though only that call - the ledger is untouched and real withdrawals continue to work, so this is an input-validation gap rather than a denial of service. The more notable consequence is that a zero-amount deposit is a free trigger for the strategy's pre-deposit hook, which can be fired repeatedly without moving any value.
Resolved
#4 low Issue
Missing zero-address validation on configuration setters
The gateway address and the asset address can both be set to the zero address, and the initialiser performs no validation on either. A zero gateway makes every deposit and withdrawal permanently unreachable, because the access check can never pass again. A zero asset makes the vault interact with an address that has no code, where the transfer helper reports success without moving anything, so the ledger drifts away from reality with no error surfaced.
Resolved
#5 low Issue
The strategy setter writes state after its external calls and has no reentrancy guard
Attaching a strategy calls into the outgoing strategy to unwind it, then writes the new strategy to storage, then approves and funds the new strategy. External calls therefore occur both before and after the state write, and the function carries no reentrancy protection even though the deposit and withdrawal paths do. A cooperating outgoing strategy can re-enter while the stored pointer still refers to it. The exposure is bounded because only the owner can start the sequence, which is why the rating stays low, but the ordering is the wrong way round for a function that moves the entire balance.
Pending
#6 low Issue
Initialisation is unprotected against front-running and emits no event
The initialiser is a plain public function guarded only by the one time initialiser modifier. If the proxy is deployed without being initialised in the same transaction, anyone can call it first and become the owner of a vault whose address has already been published. The implementation does disable its own initialisers in the constructor, which correctly prevents the implementation itself from being claimed, so the exposure is limited to the deployment procedure rather than to the code. No event is emitted, so the resulting configuration cannot be verified from logs afterwards.
Acknowledged
#7 low Issue
Upgrade authorisation is entirely off-contract and has no delay
The contract is written for a proxy - it uses the upgradeable base contracts, disables its own initialisers and reserves a block of storage slots - but it inherits no upgrade module, so there is no upgrade entry point and no authorisation hook anywhere in the audited code. Whoever controls the external proxy administrator can replace the implementation at will, with no delay, no event at this layer and no record visible from the vault itself. The documentation does not state which proxy pattern is intended, so the real upgrade authority could not be assessed as part of this review.
Acknowledged
#8 low Issue
Configuration guards check the recorded total rather than the actual balance
The asset setter and the sentinel setter both permit a change only while the recorded total is zero. That figure is the vault's own bookkeeping, not its real holdings. A vault can hold stray tokens, residual dust from a fee charging asset, or funds a strategy returned outside a withdrawal, and still report a zero total. The guard therefore allows a configuration change that abandons real value, and the shortfall described in the fee-on-transfer finding makes the two figures diverge during ordinary operation rather than only in unusual cases.
Pending
#9 low Issue
The pre-deposit hook observes a native balance that already includes the incoming deposit
On the native path the vault calls the strategy's pre-deposit hook from inside a payable function, so the value attached to the call is already part of the vault's balance when the hook runs. A hook that reads the vault's balance to decide how much to harvest, or to price anything, sees a figure inflated by the deposit currently being processed. The token path does not behave this way, because there the transfer happens after the hook. The practical impact depends entirely on what a given strategy does inside the hook, but the inconsistency between the two paths is not something a strategy author would expect.
Pending
#10 low Issue
Inconsistent and floating compiler pragmas across the codebase
One interface accepts any compiler from the 0.8 series while the other three files require at least 0.8.24. A floating pragma means the same source can be compiled by different compiler versions with different code generation and different known issues, which undermines reproducible builds and makes verification against deployed bytecode harder than it needs to be. The contract also relies on transient storage and therefore needs a chain that supports it, and nothing in the source enforces that beyond the pragma range.
Pending
#11 low Issue
Strategy unwind during migration is never verified, unlike the migration itself
Migration became asymmetric. Handing funds to the incoming strategy now asserts that the strategy registered them, and the whole call reverts if it did not, but recalling funds from the outgoing strategy has no equivalent assertion. The amount to recall is taken from the outgoing strategy's own reported balance and the return value of the recall is not checked, so a strategy that reports a position it does not hand back causes the funds to stay where they are while the configuration change reports success. Afterwards the vault points at the new strategy, the ledger still records the full liability, the reported balance has dropped to zero, and users cannot withdraw. Reproduced with a strategy that clears its bookkeeping and returns successfully without transferring anything: one thousand tokens are left in the old strategy, the ledger is untouched, and the withdrawal fails. This requires a misbehaving or partially compromised strategy, which is the same trust class as the delivery gap that the revision closed on the withdrawal path, so the same treatment is warranted here.
Pending
#12 low Issue
A native-mode vault can no longer be funded from any source other than its attached strategy
Narrowing the fallback receiver to the attached strategy also closed every benign inbound route. In native mode this leaves no way at all to put value into the vault: the deposit entry point credits a ledger position, so it cannot be used as a backstop, the fallback receiver rejects everyone except the current strategy, and the native rescue function deliberately reverts in native mode. The consequence appears exactly when it is least welcome. After an emergency detach the strategy pointer is zero, so the previously attached strategy can no longer return funds even once it is healthy, and the owner cannot cover the shortfall out of pocket. Recovery requires re-attaching the same strategy and then detaching it through the ordinary path, which is a working but undocumented sequence that depends on the broken component becoming healthy again. Reproduced end to end: after detaching a frozen native strategy, the owner's transfer to the vault reverts, the rescue path reverts, and the user's withdrawal reverts for want of liquidity.
Resolved
#13 low Issue
The reward claim path is an unconstrained call into the strategy
The reward channel added for the yield decision calls the strategy's claim function with an owner-supplied recipient and neither bounds what may be transferred nor checks anything afterwards. What counts as a reward is therefore defined entirely by the strategy. A strategy that treats its whole balance as claimable, whether by design, by bug or after being upgraded, will hand the depositors' principal to the recipient the owner names, and the vault will emit a reward event and leave the ledger untouched, so nothing in the vault records that the liability is no longer covered. Reproduced with a strategy whose claim transfers its entire balance: one thousand tokens of principal leave through the reward channel while the ledger still shows the full amount owed. This overlaps the standing owner-trust exposure, but it is quieter than swapping the strategy: it changes no configuration, needs no migration, and can be repeated.
Pending
#14 low Issue
The deposit pause does not extend to configuration changes
The pause was scoped to deposits alone. Keeping withdrawals out of it is the right decision and is well argued in the code, but leaving configuration out of it means the most consequential privileged action stays fully available during an incident. A paused vault will still accept a strategy migration that grants an unlimited allowance and pushes the entire balance into the new target. Verified: with deposits paused, attaching a hostile strategy still moves the whole balance in one transaction. The guardian's ability to pause therefore stops new money arriving but does nothing to contain an incident that involves the owner key, which is the scenario a guardian role usually exists for.
Pending
#15 low Issue
Surplus retained by the withdrawal path can never be moved out of the vault
Relaxing the delivery post-condition to a lower bound was the right change, and the contract states that the resulting over-delivery stays in the vault as protocol funds. Nothing can move it out again. The surplus is never added to the ledger, so no withdrawal can reach it; the reward channel operates on the strategy rather than the vault, so it cannot see it; and the token rescue refuses the configured asset by design, which is the control that stops the owner reaching user deposits. In native mode the native rescue reverts outright for the same reason. The result is that the one place the contract deliberately accumulates protocol revenue is the one place it cannot be collected from, and the source comment describing the reward claim as the single auditable channel through which surplus leaves the strategy is no longer accurate, because the withdrawal path has become a second and unauditable one. Reproduced in both modes: a strategy that returns five more than requested leaves that amount in the vault with the ledger at zero, and the owner's attempt to sweep it is rejected. The funds are not lost to depositors, since they serve as liquidity for the next withdrawal, but they are outside the accounting and outside the protocol's reach. A convoluted recovery does exist, by detaching the strategy and pointing the vault at a different asset so that the residue stops being the configured one, but that is a side effect rather than a designed path.
Pending
#16 low Issue
Withdrawals covered by vault liquidity are paid without any delivery post-condition
The rewritten withdrawal enters the strategy only when the vault cannot cover the amount on its own, and the delivery post-condition sits inside that branch. When the vault can cover it, the payment is a single unchecked transfer and nothing verifies what the recipient actually received. For an asset that behaves normally this is correct and is cheaper than checking. For an asset that shortens transfers it means the ledger is debited in full while the user is paid less, silently. The previous revision had the same blind spot only when no strategy was attached at all, because an attached strategy always forced the payment through the checked branch; this revision widens it to every withdrawal the vault can cover from its own balance, which the same revision deliberately makes more common by retaining surplus and by leaving the remainder of a capped deposit idle. Reproduced with a one percent fee asset and a strategy that accepts nothing: a position of 3,960 is debited in full and the depositor receives 3,921. This is bounded by the deposit path rejecting fee-taxed assets, so it is reachable only for an asset whose transfer behaviour changes after deployment or for amounts small enough that the fee truncates to zero, which is recorded separately.
Pending
#17 low Issue
Reward claim is bounded against the pre-claim balance, so yield the strategy reports can never be claimed
The bound added to the reward channel requires the strategy's reported balance not to have fallen across the claim. That correctly blocks a claim which reaches into principal, but it also blocks a claim which moves nothing but yield, whenever that yield is part of what the strategy reports. The strategy interface specifies the reported balance as the idle and deployed assets held by the strategy, so accrued yield denominated in the vault asset sits inside it, and a well-behaved strategy forwarding only its surplus necessarily lowers the figure and trips the check. The channel therefore cannot perform the task it was added for, and the decision that yield belongs to the protocol has no working route to collect it. Where the check does pass is where the reward is a different token that the reported balance never counted, in which case the bound is denominated in the wrong asset and constrains nothing. Reproduced with a strategy that reports principal plus accrued yield and claims only the difference: the claim reverts although principal was never at risk, and the recommended bound would have allowed it. Depositors are unaffected either way, because the stranded yield still serves as strategy liquidity and principal is paid in full, so this is a protocol revenue defect rather than a solvency one.
Pending
#18 low Issue
An asset that shortens transfers is rejected per amount rather than per asset, so dust deposits are accepted and then freeze
The deposit check rejects an asset by comparing the received amount against the requested one, which makes the rejection a property of the amount rather than of the asset. A proportional fee truncates to zero below a threshold, so deposits under that threshold are accepted and credited in full. They accumulate into a position whose own size is above the threshold, at which point the exit does incur a fee and the withdrawal reverts, leaving the position stuck at a size that can only be withdrawn in pieces small enough to be untaxed. Reproduced with a one percent fee asset, where forty deposits of 99 are each accepted because the fee on 99 truncates to zero, and the resulting position of 3,960 cannot be exited in one call while a 99 sized exit still succeeds. This is not introduced by the third revision and reproduces identically against the previous one; it surfaced now because the payability property was restated in terms of the exit actually succeeding. Solvency is unaffected throughout, and the case requires the vault to be configured with an asset the project already documents as unsupported.
optimization Issues | 8 findings
Pending
#1 optimization Issue
Repeated storage reads of the asset, strategy and sentinel within a single call
The asset, strategy and native sentinel variables are each read from storage several times inside the same function. The balance accessor reads the asset and the strategy up to four times between them, and the deposit and withdrawal paths repeat the same reads. Each repeat is a full storage load that the compiler cannot eliminate because external calls sit in between.
Resolved
#2 optimization Issue
The combined balance accessor is invoked twice to size a single deposit
Sizing a single deposit calls the combined balance accessor twice, and each of those calls performs a token balance query plus a call into the strategy. Four external calls are made to establish a number that could be obtained from the vault's own balance with two.
Pending
#3 optimization Issue
Long revert strings instead of custom errors
Requirement checks throughout the vault and the transfer helper use string messages, several of which exceed thirty two bytes and therefore occupy multiple words of both deployment code and revert data. There are fourteen such checks across the codebase, eight of them with strings long enough to spill into a second word.
Pending
#4 optimization Issue
Public functions that are never called internally
The initialiser and both entry points are declared public but are never called from inside the contract. A public function has to copy its arguments into memory to support internal invocation that never happens here.
Pending
#5 optimization Issue
Single-member struct forces a struct load on every balance access
Per-user balances are held in a struct with one member, so every read and write goes through a struct load rather than reading the slot directly. The wrapper provides no packing benefit because the single member already occupies a full slot.
Pending
#6 optimization Issue
Long-form assignment instead of compound assignment on accumulators
Ledger updates are written in long form, reading the variable, adding or subtracting, then writing it back, rather than using the compound assignment operators. On the withdrawal path the two subtractions are also guarded by a preceding bound check, so their overflow checks are redundant.
Pending
#7 optimization Issue
Internal transfer helpers with a single call site each
The two internal helpers that push funds to the strategy are each called from exactly one place, and each re-reads the strategy from storage even though the caller has just read it. The indirection costs a jump and a redundant storage load without improving readability.
Pending
#8 optimization Issue
Redundant type casts on variables that already hold that type
The asset and strategy variables are repeatedly wrapped in casts to the interface types they already hold. There are eight such casts across the contract.
informational Issues | 16 findings
Pending
#1 informational Issue
Unused approval helper that lacks the reset non-standard tokens require
The approval helper in the shared transfer library is never called by any contract in scope, because the vault uses the forced approval routine from its dependency instead. It is also the one helper that most needs care, since it writes a new allowance without first resetting it to zero, which several widely used tokens require. Dead code that is weaker than the live path is a hazard rather than a neutral leftover.
Resolved
#2 informational Issue
Storage gap size is unconventional and undocumented
The reserved storage gap is fifty slots sitting on top of six declared slots, giving a fifty six slot footprint. The usual convention sizes the gap so the total reaches a round number, which would be forty four here. Either choice works, but the value chosen should be written down, because the only thing preventing a future upgrade from corrupting user balances is that whoever adds state remembers to shrink the gap by the same amount.
Pending
#3 informational Issue
Documentation asserts security properties the code does not provide
The contract header claims that fee-on-transfer tokens are handled by measuring actual received amounts, which is only half true because the second transfer hop is never measured, and it claims that fund stranding is prevented, which does not hold when the strategy is unhealthy. The comments also carry finding identifiers from a previous review that mean nothing to a reader of this codebase. Both patterns encourage a reviewer or integrator to trust a guarantee that is not there.
Pending
#4 informational Issue
Public parameters use the internal naming convention
Parameters on public and external functions are prefixed with an underscore, which by widespread convention marks internal or private identifiers. There are twelve such cases across the contract. The effect is purely cosmetic but it makes the externally facing interface read like internal plumbing, which slows down anyone integrating against it.
Pending
#5 informational Issue
Configuration events omit the previous value
Every configuration change emits an event carrying only the value that was set. Off-chain monitoring therefore has to reconstruct the previous value from earlier logs, which is fragile and impossible if any log was missed. For a contract where a single configuration change can move every asset it holds, the audit trail should be self contained.
Pending
#6 informational Issue
Single-member struct adds indirection without purpose
The per-user record is a struct containing a single amount. That shape suggests further fields were planned - a deposit timestamp, a share count, a reward debt - and never added. In its current form it adds a layer of indirection to every read and write without providing anything in return.
Pending
#7 informational Issue
Borrows tokenised-vault vocabulary without implementing the standard
The vault uses the vocabulary of the tokenised vault standard - assets, total assets, balance - without implementing that standard or any token interface at all. An integrator familiar with the standard will reasonably assume that the total assets figure reflects the value backing user claims, which here it does not, because yield is never attributed to anyone. Borrowing the names without the semantics is how integration bugs start.
Pending
#8 informational Issue
Deployment depends on chain features that are not enforced anywhere
The reentrancy protection uses transient storage, so the contract requires a chain that supports the Cancun rules, and the compiler settings also require at least the Shanghai rules for the zero push instruction. Neither requirement is enforced outside the compiler pragma. Deploying to a chain that lags behind on either would produce a contract whose critical paths revert, and the documentation names a chain without stating the constraint.
Acknowledged
#9 informational Issue
Interface declares functions that no consumer ever calls
The strategy interface declares a harvest function, a reward claim function and an asset accessor, and the vault calls none of them. The asset accessor is the one that would have prevented a mismatched strategy from being attached, and the harvest and claim functions are the ones that would have brought yield back to depositors. Unused interface surface leaves strategy authors guessing at what the vault will actually invoke.
Pending
#10 informational Issue
Asset variable is typed as a token but can hold a non-token sentinel
The asset is declared with a token type, but in native mode it holds a sentinel address that is not a token at all. Every code path that touches it therefore has to remember to check the operating mode first, and any path that forgets will revert when it calls a token method on a non-token. The type is telling the reader something that is not true half the time.
Acknowledged
#11 informational Issue
Public balance accessors can return mid-transaction state
The balance accessors are documented as integrator facing, but they combine the vault's own holdings with a figure the strategy reports, so a call made while a strategy interaction is in progress returns a partially updated view. An integrator that treats either accessor as a price or valuation source inside a callback will read an inconsistent state. Nothing in the contract prevents that, and nothing in the documentation warns against it.
Acknowledged
#12 informational Issue
License declared as unlicensed across all files
All four files in scope declare themselves as unlicensed. That is consistent and may well be deliberate, but it is worth a conscious decision for a contract that users are being asked to place funds in, since it affects whether third parties can legitimately fork, review or build tooling against the code.
Pending
#13 informational Issue
No build configuration or dependency lockfile in the repository
The repository contains the contract sources but no build configuration and no dependency lockfile, even though the header comments reference a build config file that is not present. Dependency versions therefore could not be taken from the project and had to be pinned during this review at the current major release of the dependency library. Any difference between that choice and the version used for deployment would change the compiled output.
Pending
#14 informational Issue
A public configuration setter was renamed, breaking the previously reviewed interface
The sentinel setter was renamed between the two revisions, which changes its function selector. Any deployment script, monitoring configuration, subgraph or operations runbook written against the reviewed interface will fail against the new one, and the failure mode is a silent no-match rather than a compile error for anything that calls by selector. The rename was also only partial: the function and its parameter use the new name while the state variable and the event it emits keep the old one, so the same concept now appears under two names in one contract. A private deposit helper was renamed in the same change, which is harmless but contributes to the same inconsistency.
Pending
#15 informational Issue
Superseded ownership base is still imported alongside its replacement
The contract now inherits the two-step ownership base, which itself extends the single-step one, but the single-step base is still imported by name and the new import was added with a path-style import rather than the named style used by every other import in the file. Neither affects behaviour: the redundant named import is unused and the mixed style is cosmetic. Both make the inheritance chain harder to read than it needs to be, and a path-style import pulls every symbol in the imported file into scope, which is what the named style elsewhere in the file exists to avoid.
Pending
#16 informational Issue
The reentrancy guard is not the outermost modifier on the new owner functions
Three owner functions carry the access-control modifier before the reentrancy guard, so the access check runs outside the guard. There is no impact here, because the access-control modifier only reads storage and makes no external call, so there is nothing for a re-entrant caller to exploit in the window. It is flagged because the convention exists precisely so that the property does not have to be re-derived every time a modifier is added or changed, and a future modifier that does call out would silently sit outside the guard.