News | OpenZeppelin

Vesu V2 Differential Audit

Written by OpenZeppelin Security | September 30, 2025

Summary

Type: DeFi
Timeline: September 1, 2025 → September 12, 2025
Languages: Cairo

Findings
Total issues: 24 (21 resolved)
Critical: 0 (0 resolved) · High: 2 (2 resolved) · Medium: 3 (3 resolved) · Low: 5 (5 resolved)

Notes & Additional Information
14 notes raised (11 resolved)

Scope

OpenZeppelin performed a differential audit of the vesuxyz/vesu-v2 repository at commit 7a848ce against commit 7b8be9d.

In scope were the following files:

 src
├── common.cairo
├── data_model.cairo
├── interest_rate_model.cairo
├── lib.cairo
├── math.cairo
├── oracle.cairo
├── packing.cairo
├── pool.cairo
├── pool_factory.cairo
├── units.cairo
└── v_token.cairo

Update: The final state of the audited codebase, including all implemented resolutions, is reflected in commit dd868d5. In this version, the Emergency Shutdown mechanism is removed. Related issues are resolved, and the required invariants are now enforced directly in each protocol action, per asset pair, isolating checks so that violations in one pair do not affect others.

System Overview

Vesu is a novel, fully open and permissionless DeFi lending protocol. It builds upon the pooled liquidity model popularized by successful lending protocols but does so without governance intermediaries. Instead of relying on vulnerable and inefficient governance processes, Vesu allows anyone to create lending pools, letting the market itself determine the liquidity distribution. Ultimately, Vesu aims to become the credibly neutral lending settlement layer that DeFi lending markets have lacked, combining scalability and security.

Core Operations

Lending and Borrowing

Users interact with Vesu through lending pools, which are composed of lending pairs:

  • Lending Pair: A unidirectional agreement where a specific collateral asset can be deposited to borrow a specific debt asset.
  • Each pair enforces a fixed loan-to-value (LTV) ratio through liquidations.

Liquidity within a pool is shared across all pairs in that pool, maximizing capital efficiency. Risks, however, are strictly isolated between pools.

Position Management

User activity is tracked through positions, which record:

  • Collateral Shares: A user’s proportional claim on pooled liquidity.
  • Nominal Debt: The outstanding debt excluding accrued interest.

Interest accrues per asset via a rate accumulator, ensuring efficient accounting across many positions.

Interest Rate Model

Vesu implements a fully autonomous adaptive interest rate model, inspired by Fraxlend. It combines the following components:

  • A utilization-based rate curve.
  • A controller that adjusts curve parameters in response to imbalances.

This approach achieves simplicity, robustness, and proven market performance.

Oracles

Vesu integrates with Pragma oracles for price feeds, enabling pool-solvency checks. Prices are computed using the median of multiple data sources, with contextual data (e.g., provider count, update freshness, etc.) for additional validation.

Security Model and Trust Assumptions

The security of the Vesu protocol depends on the correct operation of its pools, parameters, and oracle systems.

  1. Curator Trust: Each pool is managed by a curator who sets critical parameters (e.g., liquidation factors, oracle integrations). Users must trust the curator to configure pools safely.
  2. Oracle Reliability: Oracles can return prices based on historical TWAPs. In volatile markets, this lag can introduce risk. Invalid spot prices may propagate into TWAPs, reducing safety.
  3. Parameter Calibration: Pool solvency depends heavily on correctly set TVL values and liquidation factors. Poor configuration can allow harmful arbitrage or weaken liquidation incentives.

Despite these assumptions, Vesu is designed to ensure that no centralized governance can unilaterally change markets, except via predefined curator.

Privileged Roles

The following privileged roles were identified in the Vesu protocol:

  • owner: Defined in the pool factory and passed to all deployed pools and oracles. Can upgrade pools, upgrade oracles, and pause/unpause pools.
  • curator: The primary operator of a pool, responsible for setting parameters, adding assets, choosing oracle implementations. Users must trust the curator of any pool they join.
  • manager: Manages oracle implementations by registering new assets and adjusting their configurations.
  • shutdown_mode_agent: Transitions pools from normal operation into recovery mode, enforcing solvency protections. This role was removed along with the Shutdown mode.
  • pausing agent: Authorized to pause (but not unpause) pools under abnormal conditions. This role was introduced during the fix review phase.

High Severity

User Collateral Will Be Permanently Stuck in Redemption Mode

When a pool shuts down, the final state that is not further transitionable to any other state is the "redemption mode". This mode only permits decreasing a position's collateral and does not allow for either increasing or decreasing debt. Particularly, this state has an assertion which ensures that the position being modified has to hold a nominal debt of 0. However, since debt repayment is not allowed, if a user did not completely repay the debt during previous modes, they will not be able to withdraw their collateral ever again. Since liquidations are also not allowed in this mode, it becomes impossible to bring the debt down to 0.

Consider allowing debt to be repaid in the redemption mode.

Update: Resolved in pull request #63. The Vesu team stated:

that they removed the shutdown mechanism; in V2, since contracts are upgradeable, the owner can implement this logic when a pool is wound down.

Claiming Fees Does Not Decrement Total Collateral Shares or Reserves

When the amount of shares to account for the fee recipient is computed, both the amount of total collateral shares and the amount of fee shares are incremented. However, when fees are claimed, only the fee shares are reset to 0 and the total collateral shares are not decremented at all. In addition, the code does not decrement the reserves which is where the liquidity is taken from. This leads to having an incorrectly stored amount for the reserve which affects utilization computation, collateral withdrawal, and debt-borrowing limits.

Consider decrementing both the total collateral shares and the reserves when claiming fees for the fee recipient.

Update: Resolved in pull request #64.

Medium Severity

Loss of Funds for the Fee Recipient During Fee Claiming

The claim_fees function can be called by anyone, and it converts the total amount of collateral shares accounted for as fees into collateral assets, rounds the value down, and sends it to the fee recipient. The collateral shares counted as fees are computed in each interaction with the protocol. So, if the steps from one timestamp and the other are small, given that the fee rate will be also set to a low percentage, the collateral shares accounted for as fees will be pretty small as well. Taking into account the fact that anyone can execute this function at any time, if the function is called often, the collateral shares converted into assets, rounded down, can lead to a loss of funds due to this rounding. Specifically, with assets that have less than 18 decimals, there will be a lot of shares per asset that will amplify this issue.

Consider restricting the access to the claim_fees function to either the curator or the fee recipient.

Update: Resolved in pull request #68.

Claiming Fees Bypasses Shutdown Mode

The claiming fees represent a decrease of collateral. As such, this operation should only be allowed in normal and redemption mode. When the shutdown state is in recovery or subscription mode, users are not allowed to decrease his collateral. However, fees can still be claimed regardless of the shutdown state. This bypasses the invariant of recovery and redemption state. In addition, since the operation is permissionless, it can be triggered by anyone.

Consider ensuring that the shutdown state is in normal or redemption mode when claiming fees.

Update: Resolved. The Vesu team stated:

Fixed. Shutdown mechanism has been removed completely, and pausing also applies in claim_fees.

ERC-4626 Inconsistencies in VToken

Multiple ERC-4626-related inconsistencies were identified in the VToken contract:

  1. The can_deposit and can_withdraw functions do not check if the pool is paused. This affects the "max" and "preview" functions of all methods. For example, if the pool is paused, max_deposit will return a maximum amount of assets to deposit, but when an external integrator would try to deposit, it would revert. Whereas, if max_deposit were to return 0, the external integrator would not try to deposit anything directly.
  2. ERC-4626 specification recommends implementing a permit function to improve the UX of approving shares on various integrations.
  3. When creating VTokens, the name and symbol values have to be provided in a felt252 data unit. This limits the string length to 31 which deviates from typical ERC-20 implementations that allow for longer strings. Consider using ByteArray for token name and symbol to maintain full ERC-20 compatibility.
  4. The max_withdraw and max_redeem function return wrong values when the pool is not in a normal state. The calculate_withdrawable_assets function returns 0 when the pool's asset utilization exceeds the maximum. However, this invariant is only enforced in the pool for the normal shutdown state. Hence, when the pool transitions to a different shutdown state, the max utilization will not be checked but the VToken's methods will still take this into account. The worst scenario would be the pool being in redemption mode where debt cannot be repaid, and only collateral can be withdrawn. In such a case, the utilization can only increase and, once it reaches the maximum, max_withdraw and max_redeem will return 0 whereas, in fact, the VToken's position can still withdraw collateral.

Consider addressing the aforementioned inconsistencies to achieve a consistent and ERC-4626-compliant VToken implementation.

Update: Resolved in pull request #78, at commit ac495a3. The Vesu team fixed 1, 3, and 4, and acknowledged 2, since the permit pattern is not used on Starknet.

Low Severity

Pool Factory add_asset Function Lacks Access Control

Adding new assets to a pool is a permissioned action that can only be executed by the curator. However, the same method from the pool factory, that also allows for registering VTokens that are assigned to these new asset pairs, does not have any access control.

The process works as follows:

  1. The curator must nominate the pool factory as the pending curator.
  2. The add_asset function is executed in the pool factory.

The logic works fine as long as these 2 steps are performed atomically. However, if the curator first executes step 1 without atomically executing step 2, it opens up a window for anyone to call the add_asset function with arbitrary data.

Consider adding a check to ensure that the caller is the pool's curator.

Update: Resolved at commit 813fc31.

Incorrect Handling of 0^0 in pow() Math Function

The pow function returns 0 when called with (0,0). This diverges from Solidity conventions, where 0**0 is defined as 1. Such an inconsistency can cause unexpected behavior if the function is reused in other contexts.

Consider updating the function so that it computes pow(0,0) = 1.

Update: Resolved at commit d43f2e8.

Fees Can Become Stuck

The way to account for fees for the fee_recipient is by storing the amount of collateral shares computed in each protocol interaction. The way that these fees are claimed is through the claim_fees function. This operation extracts all the shares that have been accounted for as fees and tries to withdraw the collateral assigned to the amount of shares. However, the issue with this approach is that it assumes that the entire collateral of an asset is held liquid by the pool, whereas only a small amount of it is held by the pool and the rest is accounted for as debt. Hence, if this function is not executed quite often and the amount of shares grows to exceed the percentage of liquid collateral of an asset, the function will always revert because there will never be enough funds to be transferred.

Consider allowing the caller to provide an amount of shares to claim as fees, such that if the aforementioned scenario were to materialize, fees can still be withdrawn in parts.

Update: Resolved in pull request #69.

Division by Zero in Interest Rate Calculation

The assert_interest_rate_config function only validates that min_target_utilization <= target_utilization but allows min_target_utilization to be zero. When min_target_utilization is 0, the computation that updates the interest rate triggers a division by zero. This causes the pool to panic and become unusable, and all operations that update rate accumulators (deposits, withdrawals, borrowing, and liquidations) will revert.

Consider adding a check to disallow min_target_utilization = 0.

Update: Resolved in pull request #70.

Possible Denial of Service During Pool Deployment

Pool deployment is a permissionless action that is performed via the pool factory. The only differentiators between deployments are the name and the oracle implementation. This can lead to deployment collisions because the oracle will often be the same implementation. A malicious user could make a pool deployment fail simply by creating a pool with the same name and the same oracle implementation before the legitimate one.

Consider adding an incrementing counter that increases with each pool deployment and using this counter as a salt to prevent deployment collisions.

Update: Resolved in pull request #71.

Notes & Additional Information

Misleading Error Message

In oracle.cairo, when retrieving the price of an asset, if the asset has a pragma_key that is not configured, it fails with oracle-price-invalid. This can be misleading because the price is not invalid but has not been set.

Consider changing the error message to oracle-price-not-set.

Update: Resolved at commit 2e4c221.

Unchecked Return Value From Approve

When deploying the v_token contract, a max approval for the underlying asset to the pool is executed. However, the return value of approve is ignored, which can silently misconfigure the contract. If the approval is not correctly set, the v_token contract will remain unusable and cannot be updated in the pool factory.

Consider explicitly checking that the returned value is true.

Update: Resolved at commit 28f3a1c.

Intended Panic in pow_scale

The pow_scale function panics with divide-by-zero when is_negative and x == 0 are true. This check is mathematically valid (0^-n is undefined) but relies on Cairo’s implicit panic. As such, the failure mode may appear as a generic runtime error.

Consider adding an explicit assertion for clarity:

 if is_negative {
    assert!(x != 0, "zero base with negative exponent");
    x = SCALE * SCALE / x;
}

Update: Resolved at commit 0967147.

Class Hashes Cannot Be Updated

The pool_factory contract cannot be upgraded and has three class hashes for the pool, oracle, and VToken implementations that are set upon construction and are not mutable. However, it is likely that the pool and oracle components will require updates over time.

Consider either adding a setter function for these class hashes that is only accessible by the owner, or making these three class hashes constant within the pool_factory contract.

Update: Resolved at commit 55c6ff2. The Vesu team stated:

that the pool_factory is now upgradeable

Use unwrap_syscall When Deploying New Contracts

When the pool_factory contract deploys new pool instances, it uses the unwrap() method to make the execution fail if the deployment fails. For a more specific unwrapping method, the unwrap_syscall should be used which is intended to be used for all Cairo syscalls.

Consider using unwrap_syscall when deploying pools, oracles, and VTokens in the pool factory.

Update: Resolved in pull request #73.

Incomplete Documentation

Throughout the codebase, multiple instances of incomplete documentation were identified:

  • In pool.cairo, the add_asset function lacks documentation for interest_rate_config.
  • In pool.cairo, the calculate_collateral function does not document the precision of the collateral_shares argument. Consider adding [SCALE] precision.
  • In pool_factory.cairo, the transfer_inflation_fee function lacks documentation.

Consider thoroughly documenting all functions and events, including their parameters and return values, that form part of a contract’s public API.

Update: Resolved in pull request #74.

Unnecessary Zero-Address Check

In oracle.cairo, the accept_manager_ownership function ensures that the caller is the pending_manager for the call to succeed. However, an additional check is performed to ensure that the pending_manager is not the zero address. This is unnecessary because it is not possible for the zero address to call this function.

Consider removing the aforementioned zero-address check.

Update: Resolved in pull request #75.

Incorrect Documentation

Throughout the codebase, multiple instances of incorrect documentation were identified:

Consider addressing the above-listed instances of incorrect documentation to improve the clarity and maintainability of the codebase.

Update: Resolved in pull request #76.

Lack of Indexed Event Parameters

In oracle.cairo, multiple instances of events not having any indexed parameters were identified:

Consider indexing event parameters to improve the ability of off-chain services to search and filter for specific events.

Update: Resolved in pull request #77.

Excess Asset Withdrawal in VToken

When a withdrawal request specifies more shares or more assets than a position is holding, the pool automatically redeems the maximum available. Within the withdrawal method of the VToken contract, users specify the exact amount of assets they wish to receive. If the requested asset amount exceeds what the vault’s shares can redeem, the pool will still redeem all shares, resulting in fewer assets returned than requested. However, the withdrawal method implementation transfers the full user-specified amount, enabling a malicious user to withdraw more assets than they are entitled to.

Consider transferring the amount of assets returned from the modify position call.

Update: Resolved at commit 1f810f7.

Lack of Liquidation Safety Threshold

There is only a single limit which determines if a position is either collateralized or can be liquidated. This check is implemented in the is_collateralized function. If it returns true, the modify_position function allows the changes to the position and liquidation is not possible. However, if it returns false, liquidation can be performed. The fact that there is no safety zone between collateralization and being eligible to be liquidated makes it unsafe for the user to bring their health factor up before being liquidated.

Consider adding a threshold between collateralization and liquidation such that if a user gets their position undercollateralized, they have the opportunity to bring their debt down before being liquidated.

Update: Acknowledged, not resolved. The Vesu team stated:

that there is no safety issue in managing positions; the buffer between max-LTV and liquidation-LTV only affects creation, which the UI already controls.

Debt Cap Can Be Bypassed Through Interest Accrual

The debt-cap mechanism of the pool validates new debt creation but allows unlimited growth through interest accrual, defeating its risk-management purpose.

Consider switching the debt cap to nominal debt, which will not increase over time without external interaction.

Update: Acknowledged, not resolved. The Vesu team stated:

that debt still continues to increase and should just be documented properly.

Asset Scale Packing Mismatch With 0 Decimals

When adding new assets to a pool, the scale is computed based on the decimals of the ERC-20 token being added. For specific tokens such as the GoL2: TGOL token (ranked 8 among ERC-20 tokens on Starknet), the decimals value is 0. In this case, the computation results in 1 to avoid breaking subsequent calculations. However, during packing and unpacking, the value becomes 0 due to the use of log_10_or_0 and pow_10_or_0. This causes later computations to fail.

Consider using different log and pow functions to handle the aforementioned edge case.

Update: Acknowledged, not resolved. The Vesu team stated:

that this is an intended behavior; protocol does not support 0-decimal tokens and assumes standard token properties, with trust on the curator for compatibility.

Asset State Violation Forces All Asset Pairs to a Recovery State

Whenever an asset has an unsafe rate accumulator or returns an invalid price from the oracle, it will make the pool to transition into a Recovery state. This affects all asset pairs in the pool even if their rate accumulator is safe and return valid prices. This will prevent users from assets that are working correctly from decreasing collateral or increasing debt. This issue can be particularly impactful for pools with a lot of different assets. It will just need one of them to violate the configuration to force all other to move into a Recovery state.

Consider having a shutdown state for each pair such that when an assets has a violation, only the specific pair will move to a Recovery state.

Update: Resolved in pull request #63. The Vesu team stated:

that they removed the shutdown mechanism completely. Invariants apply only to assets in position updates, and pools must be paused manually if needed. In the initial design, the intent was for the entire pool to transition into Recovery mode if a single pair became untrusted.

Conclusion

The audit focused on the changes introduced in Vesu v2, a fully open and permissionless DeFi lending protocol that expands upon the pooled liquidity model while eliminating governance intermediaries. Key features of the v2 protocol include the introduction of a pool factory to create new lending pools, the issuance of VTokens tied to pool assets, and the integration of an oracle system.

While the protocol exhibits a generally robust design, one high-severity issue was identified during the audit pertaining to the internal accounting of reserves and collateral shares during fee claims.

Overall, the underlying codebase was found to be well-structured, and the mathematical models powering the protocol have been verified as sound. No critical-severity attack vectors were identified that could lead to liquidity drainage or compromise the protocol’s security.

The Vesu team is appreciated for their proactive approach throughout the audit process. Their collaborative and transparent communication facilitated a thorough examination of the protocol. We appreciate the opportunity to conduct this audit and look forward to further collaboration as the project evolves.