Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Invalid

Non-compliance with EIP-4626 standards in ZlpVault

Summary

The ZlpVault contract claims to follow the EIP-4626 standard for tokenized vaults, but several key functions deviate from the specification. These deviations may lead to unexpected behavior, reduced interoperability, and integration failures with other DeFi protocols relying on EIP-4626 vaults.

Vulnerability Details

1. maxMint and maxWithdraw Always Returning 0

  • The functions maxMint and maxWithdraw always return 0, making it impossible to programmatically determine available liquidity.

  • This contradicts the expected behavior in EIP-4626, where these functions should return the actual limits based on the vault’s state.

  • Even if the protocol has specific design reasons for this, returning 0 could mislead integrations into assuming no liquidity is available, potentially breaking composability.

2. _convertToAssets and _convertToShares Delegating Logic to External Contracts

  • The conversion functions rely on an external contract (MarketMakingEngine) for asset-to-share calculations.

  • While this approach may be intentional, it introduces a single point of failure and prevents the vault from performing calculations independently.

  • The Math.Rounding parameter is also ignored, which may lead to inconsistent rounding behavior, especially in edge cases.

3. Restricted Access to Core Vault Functions (deposit, mint, withdraw, redeem)

  • These functions can only be called by the MarketMakingEngine due to the onlyMarketMakingEngine modifier.

  • This restriction prevents direct user interaction, making the vault non-standard and potentially incompatible with wallets, DeFi aggregators, and automated strategies that expect permissionless deposits and withdrawals.

  • If the intent is to restrict access, alternative approaches (e.g., governance-controlled limits or whitelisted actors) may be more flexible.

4. Missing previewDeposit, previewMint, previewWithdraw, and previewRedeem

  • The vault does not implement the preview functions, which are required by EIP-4626.

  • These functions are critical for predicting transaction outcomes before execution, improving UX and preventing unexpected losses.

  • Many DeFi front-ends and smart contract integrators expect these functions to be present and may not work properly without them.

Impact

  • Protocols that assume an EIP-4626-compliant interface may fail when interacting with this vault.

  • Functions returning 0 could make it appear as if withdrawals and mints are impossible, even when liquidity exists.

  • If the MarketMakingEngine fails or changes behavior, the vault may not function correctly.

  • Missing preview functions and restricted access may frustrate users and developers, leading to lower adoption.


Tools Used

  • Manual Code Review: Analyzed Solidity code and function implementations.

  • EIP-4626 Specification: Compared against expected behavior defined in the standard.

Recommendations

1. Implement Proper Logic for maxMint and maxWithdraw

Instead of returning 0, calculate the actual available liquidity:

function maxMint(address) public view override returns (uint256) {
uint256 totalAssets = totalAssets();
uint256 depositCap = IMarketMakingEngine(\_getZlpVaultStorage().marketMakingEngine).getDepositCap(\_getZlpVaultStorage().vaultId);
return depositCap > totalAssets ? \_convertToShares(depositCap - totalAssets, Math.Rounding.Down) : 0;
}
function maxWithdraw(address owner) public view override returns (uint256) {
uint256 balanceOfShares = balanceOf(owner);
return \_convertToAssets(balanceOfShares, Math.Rounding.Down);
}

2. Handle _convertToAssets and _convertToShares Internally

Minimize reliance on external contracts and respect the rounding parameter:

function \_convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
uint256 totalShares = totalSupply();
if (totalShares == 0) return shares;
uint256 totalAssetsCached = totalAssets();
return Math.mulDiv(shares, totalAssetsCached, totalShares, rounding);
}

3. Remove onlyMarketMakingEngine Restriction from Core Functions

Allow public interaction while implementing necessary security checks.


4. Implement previewDeposit, previewMint, previewWithdraw, and previewRedeem

Ensure users can simulate transactions before executing them:

function previewDeposit(uint256 assets) public view override returns (uint256) {
return \_convertToShares(assets, Math.Rounding.Down);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.