- September 30, 2025

OpenZeppelin Security

OpenZeppelin Security
Security Audits
Summary
-
Type: DeFi
Timeline: September 1, 2025 → September 12, 2025
Languages: CairoFindings
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.
- 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.
- 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.
- 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:
- The
can_deposit
andcan_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, ifmax_deposit
were to return 0, the external integrator would not try to deposit anything directly. - ERC-4626 specification recommends implementing a permit function to improve the UX of approving shares on various integrations.
- When creating VTokens, the
name
andsymbol
values have to be provided in afelt252
data unit. This limits the string length to 31 which deviates from typical ERC-20 implementations that allow for longer strings. Consider usingByteArray
for token name and symbol to maintain full ERC-20 compatibility. - The
max_withdraw
andmax_redeem
function return wrong values when the pool is not in a normal state. Thecalculate_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
andmax_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:
- The curator must nominate the pool factory as the pending curator.
- 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
, theadd_asset
function lacks documentation forinterest_rate_config
. - In
pool.cairo
, thecalculate_collateral
function does not document the precision of thecollateral_shares
argument. Consider adding[SCALE]
precision. - In
pool_factory.cairo
, thetransfer_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:
- In
oracle.cairo
, the argument is calledpending_manager
, but the documentation saysmanager
. - In
data_model.cairo
, the documentation fordebt_cap
states that it hasSCALE
precision, but it is denominated in debt-asset units. - In
pool.cairo
, the documentation for thepairs
map states that it returns a pair configuration. This appears to have been copied frompair_config
above. - In
pool.cairo
, the documentation for thecurator
storage variable states that it stores the address of the pool owner. This is incorrect as theowner
is a different role. - In
pool.cairo
, the documentation for thecompute_liquidation_amounts
function states that liquidations are only allowed in normal and recovery modes. This is incorrect as liquidations are only available in normal mode. - In
pool.cairo
, the documentation for theshutdown_status
function lists return values that are incorrect. - In
pool.cairo
, the documentation for thecalculate_nominal_debt
function states that the returnednominal_debt
has asset-scale precision. This is incorrect as the returned value is inSCALE
precision. - In
math.cairo
, the documentation link that refers to the forked code is outdated.
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.
Ready to secure your code?