Part 2

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

Vulnerability in deposit Function Allows First Depositor to Receive Maximum Shares

Summary

The deposit function in VaultRouterBranch.sol is vulnerable to a first deposit attack. When a vault has no initial assets or shares, the first depositor receives the maximum number of shares, effectively owning the entire vault. This happens because the ERC-4626 share allocation formula defaults to granting all shares to the first depositor, as there is no prior benchmark for share price calculation. Subsequent depositors are disadvantaged, receiving fewer shares relative to their deposits.

Vulnerability Details

/// @param assets The amount of collateral to deposit, in the underlying ERC20 decimals.
/// @param minShares The minimum amount of index tokens to receive in 18 decimals.
/// @param referralCode The referral code to use.
/// @param isCustomReferralCode True if the referral code is a custom referral code.
function deposit(
uint128 vaultId,
uint128 assets,
uint128 minShares,
bytes memory referralCode,
bool isCustomReferralCode
)
external
{
if (assets == 0) revert Errors.ZeroInput("assets");
// load the mm engine configuration from storage
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// enforce whitelist if enabled
address whitelistCache = marketMakingEngineConfiguration.whitelist;
if (whitelistCache != address(0)) {
if (!Whitelist(whitelistCache).verifyIfUserIsAllowed(msg.sender)) {
revert Errors.UserIsNotAllowed(msg.sender);
}
}
// fetch storage slot for vault by id, vault must exist with valid collateral
Vault.Data storage vault = Vault.loadLive(vaultId);
if (!vault.collateral.isEnabled) revert Errors.VaultDoesNotExist(vaultId);
// define context struct and get vault collateral asset
DepositContext memory ctx;
ctx.vaultAsset = vault.collateral.asset;
// prepare the `Vault::recalculateVaultsCreditCapacity` call
uint256[] memory vaultsIds = new uint256[]();
vaultsIds[0] = uint256(vaultId);
// recalculates the vault's credit capacity
// note: we need to update the vaults credit capacity before depositing new assets in order to calculate the
// correct conversion rate between assets and shares, and to validate the involved invariants accurately
Vault.recalculateVaultsCreditCapacity(vaultsIds);
// load the referral module contract
ctx.referralModule = IReferral(marketMakingEngineConfiguration.referralModule);
// register the given referral code
if (referralCode.length != 0) {
ctx.referralModule.registerReferral(
abi.encode(msg.sender), msg.sender, referralCode, isCustomReferralCode
);
}
// cache the vault assets decimals value for gas savings
ctx.vaultAssetDecimals = vault.collateral.decimals;
// uint256 -> ud60x18 18 decimals
ctx.assetsX18 = Math.convertTokenAmountToUd60x18(ctx.vaultAssetDecimals, assets);
// cache the deposit fee
ctx.vaultDepositFee = ud60x18(vault.depositFee);
// if deposit fee is zero, skip needless processing
if (ctx.vaultDepositFee.isZero()) {
ctx.assetsMinusFees = assets;
} else {
// otherwise calculate the deposit fee
ctx.assetFeesX18 = ctx.assetsX18.mul(ctx.vaultDepositFee);
// ud60x18 -> uint256 asset decimals
ctx.assetFees = Math.convertUd60x18ToTokenAmount(ctx.vaultAssetDecimals, ctx.assetFeesX18);
// invariant: if vault enforces fees then calculated fee must be non-zero
if (ctx.assetFees == 0) revert Errors.ZeroFeeNotAllowed();
// enforce positive amount left over after deducting fees
ctx.assetsMinusFees = assets - ctx.assetFees;
if (ctx.assetsMinusFees == 0) revert Errors.DepositTooSmall();
}
// transfer tokens being deposited minus fees into this contract
IERC20(ctx.vaultAsset).safeTransferFrom(msg.sender, address(this), ctx.assetsMinusFees);
// transfer fees from depositor to fee recipient address
if (ctx.assetFees > 0) {
IERC20(ctx.vaultAsset).safeTransferFrom(
msg.sender, marketMakingEngineConfiguration.vaultDepositAndRedeemFeeRecipient, ctx.assetFees
);
}
// increase vault allowance to transfer tokens minus fees from this contract to vault
address indexTokenCache = vault.indexToken;
IERC20(ctx.vaultAsset).approve(indexTokenCache, ctx.assetsMinusFees);
// 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);
// assert min shares minted
if (ctx.shares < minShares) revert Errors.SlippageCheckFailed(minShares, ctx.shares);
// invariant: received shares must be > 0 even when minShares = 0; no donation allowed
if (ctx.shares == 0) revert Errors.DepositMustReceiveShares();
// emit an event
emit LogDeposit(vaultId, msg.sender, ctx.assetsMinusFees);
}

In the deposit function a user can inflate his shares by being the first depositor to the vault. He will recieve large shares for small amount of deposited asset.

Impact

Future users receive significantly fewer shares for their deposits, making the vault unattractive for additional investors.

Tools Used

Manual audit

Recommendations

Set a minimum initial deposit

Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[INVALID] first deposit attack

Support

FAQs

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