Part 2

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

Attacker can grief vaultrouterbranch::deposit via direct asset token transfers

Summary

A malicious actor can grief legitimate users from depositing into a vault using vaultrouterbranch::deposit by directly sending asset tokens to the vault contract, effectively “filling” the vault from the outside and preventing new deposits. Although this does not result in stolen funds, it creates a denial of service for depositors trying to use the vault normally.

Vulnerability Details

VaultRouterBranch::deposit contains the following line which allows users deposit into a vault:

// then perform the actual deposit
// NOTE: the following call will update the total assets deposited in the vault
// NOTE: the following call will validate the vault's deposit cap
// invariant: no tokens should remain stuck in this contract
ctx.shares = IERC4626(indexTokenCache).deposit(ctx.assetsMinusFees, msg.sender);

This calls ZlpVault::deposit which in turns calls ERC4626Upgradeable::deposit:

/** @dev See {IERC4626-deposit}. */
function deposit(uint256 assets, address receiver) public virtual returns (uint256) {
uint256 maxAssets = maxDeposit(receiver);
if (assets > maxAssets) {
revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
}
uint256 shares = previewDeposit(assets);
_deposit(_msgSender(), receiver, assets, shares);
return shares;
}

The bug is located in ZlpVault::maxDeposit :

/// @notice Returns the maximum amount of assets that can be deposited into the ZLP Vault, taking into account the
/// configured deposit cap.
/// @dev Overridden and used in ERC4626.
/// @return maxAssets The maximum amount of depositable assets.
function maxDeposit(address) public view override returns (uint256 maxAssets) {
// load the zlp vault storage pointer
ZlpVaultStorage storage zlpVaultStorage = _getZlpVaultStorage();
// cache the market making engine contract
IMarketMakingEngine marketMakingEngine = IMarketMakingEngine(zlpVaultStorage.marketMakingEngine);
// get the vault's deposit cap
uint128 depositCap = marketMakingEngine.getDepositCap(zlpVaultStorage.vaultId);
// cache the vault's total assets
uint256 totalAssetsCached = totalAssets();
// underflow check here would be redundant
unchecked {
// we need to ensure that depositCap > totalAssets, otherwise, a malicious actor could grief deposits by
// sending assets directly to the vault contract and bypassing the deposit cap
maxAssets = depositCap > totalAssetsCached ? depositCap - totalAssetsCached : 0;
}
}

ZlpVault::maxDeposit() function in the vault contract, which calculates the remaining allowable deposit based on depositCap - totalAssets().
The vault’s logic only checks totalAssets() against depositCap, but does not handle externally transferred tokens. Attackers can bypass the normal deposit() flow by sending tokens directly to the vault contract. Once the vault’s actual balance (totalAssets()) meets or exceeds depositCap, maxDeposit() returns 0, blocking legitimate depositors from adding new assets.

Proof Of Code (POC)

function test_griefingdeposits(
uint256 vaultId,
uint256 amount
)
external
{
VaultConfig memory fuzzVaultConfig = getFuzzVaultConfig(vaultId);
//c user who will grief deposits
address userA = users.naruto.account;
deal(fuzzVaultConfig.asset, userA, 100e18);
//c user trying to deposit into vault
address userB = users.sasuke.account;
deal(fuzzVaultConfig.asset, userB, 100e18);
//c get deposit cap of vault
uint128 depositCap = vaultsConfig[fuzzVaultConfig.vaultId].depositCap;
//c get the deposit fee of the vault
uint128 depositFee = uint128(vaultsConfig[fuzzVaultConfig.vaultId].depositFee);
amount =bound(amount, calculateMinOfSharesToStake(fuzzVaultConfig.vaultId), fuzzVaultConfig.depositCap / 2);
//c get assets sent to vault index token
uint128 depositfeeforamount = uint128(amount)*depositFee/1e18;
uint256 assetminusfee = amount - uint256(depositfeeforamount);
//c attempt to grief deposits
vm.startPrank(userA);
IERC20(fuzzVaultConfig.asset).transfer(fuzzVaultConfig.indexToken, depositCap);
vm.stopPrank();
//c check totalassets of vault
uint256 totalAssets = IERC4626(fuzzVaultConfig.indexToken).totalAssets();
console.log(totalAssets);
//c user attempts to deposits into vault
vm.startPrank(userB);
vm.expectRevert(abi.encodeWithSelector(ERC4626Upgradeable.ERC4626ExceededMaxDeposit.selector,userB,assetminusfee,0));
marketMakingEngine.deposit(fuzzVaultConfig.vaultId, uint128(amount), 0, "", false);
vm.stopPrank();
}

Impact

Denial-of-Service: Users are unable to deposit once an attacker inflates the vault’s balance, rendering the deposit function unusable. This directly undermines the vault’s usability and could erode user trust in the protocol’s deposit cap mechanism.

Tools Used

Manual Review, Foundry

Recommendations

Recommendation: Implement a Change-Detection Mechanism for ERC4626Upgreadeable::totalAssets()

Introduce a variable (e.g., ZlpVault::lastTotalAssets) that stores the vault’s previously observed total assets. Whenever ZlpVault::maxDeposit() is called:

Compute Current totalAssets(): Call ERC4626Upgreadeable::totalAssets() to get the current balance.

Compare totalAssets() with ZlpVault::lastTotalAssets.
If there is a difference, check whether the vault’s total share supply (e.g., totalSupply()) has also changed by a corresponding amount based on the share price formula. If the share supply does not reflect this change, assume the variance in totalAssets() is caused by an external transfer (i.e., not due to a legitimate deposit transaction).

If an external transfer is detected, ignore the externally inflated amount by using lastTotalAssets for deposit cap calculations (so malicious tokens sent directly to the contract do not reduce the user’s allowable deposit). Otherwise, use the newly computed ERC4626Upgradeable::totalAssets() for correct deposit cap tracking.

Once a legitimate deposit is verified (i.e., the share supply changed accordingly), update lastTotalAssets to the new totalAssets() value. This approach ensures that only valid deposit transactions (those which increase both assets and shares) will raise the vault’s internal “cap usage,” while malicious external transfers are effectively disregarded when calculating maxDeposit().

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!