Part 2

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

Audit Report for VaultRouterBranch.sol contract - Medium severity issuest

Summary

external/
└── market-making/
└── branches/
└── VaultRouterBranch.sol

Vulnerability details

**1. Front-Running in deposit() and redeem()**

Description:

The deposit and redeem functions are susceptible to front-running attacks, where an attacker could manipulate the price or state before the transaction is confirmed.

Code Affected

deposit, redeem.


Full redeem() function

function redeem(uint128 vaultId, uint128 withdrawalRequestId, uint256 minAssets) external {
// fetch storage slot for vault by id
Vault.Data storage vault = Vault.loadLive(vaultId);
// load storage slot for previously created withdrawal request
WithdrawalRequest.Data storage withdrawalRequest =
WithdrawalRequest.loadExisting(vaultId, msg.sender, withdrawalRequestId);
// revert if withdrawal request already fulfilled
if (withdrawalRequest.fulfilled) revert Errors.WithdrawalRequestAlreadyFulfilled();
// revert if withdrawal request delay not yet passed
if (withdrawalRequest.timestamp + vault.withdrawalDelay > block.timestamp) {
revert Errors.WithdrawDelayNotPassed();
}
// prepare the `Vault::recalculateVaultsCreditCapacity` call
uint256[] memory vaultsIds = new uint256[](1);
vaultsIds[0] = uint256(vaultId);
// updates the vault's credit capacity before redeeming
Vault.recalculateVaultsCreditCapacity(vaultsIds);
// define context struct, get withdraw shares and associated assets
RedeemContext memory ctx;
ctx.shares = withdrawalRequest.shares;
ctx.expectedAssetsX18 = getIndexTokenSwapRate(vaultId, ctx.shares, false);
// load the mm engine configuration from storage
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// cache vault's redeem fee
ctx.redeemFee = vault.redeemFee;
// get assets minus redeem fee
ctx.expectedAssetsMinusRedeemFeeX18 =
ctx.expectedAssetsX18.sub(ctx.expectedAssetsX18.mul(ud60x18(ctx.redeemFee)));
// calculate assets minus redeem fee as shares
ctx.sharesMinusRedeemFeesX18 =
getVaultAssetSwapRate(vaultId, ctx.expectedAssetsMinusRedeemFeeX18.intoUint256(), false);
// get the shares to send to the vault deposit and redeem fee recipient
ctx.sharesFees = ctx.shares - ctx.sharesMinusRedeemFeesX18.intoUint256();
// cache the vault's credit capacity before redeeming
ctx.creditCapacityBeforeRedeemUsdX18 = vault.getTotalCreditCapacityUsd();
// cache the locked credit capacity before redeeming
ctx.lockedCreditCapacityBeforeRedeemUsdX18 = vault.getLockedCreditCapacityUsd();
// redeem shares previously transferred to the contract at `initiateWithdrawal` and store the returned assets
address indexToken = vault.indexToken;
uint256 assets =
IERC4626(indexToken).redeem(ctx.sharesMinusRedeemFeesX18.intoUint256(), msg.sender, address(this));
// get the redeem fee
if (ctx.sharesFees > 0) {
IERC4626(indexToken).redeem(
ctx.sharesFees, marketMakingEngineConfiguration.vaultDepositAndRedeemFeeRecipient, address(this)
);
}
// require at least min assets amount returned
if (assets < minAssets) revert Errors.SlippageCheckFailed(minAssets, assets);
// invariant: received assets must be > 0 even when minAssets = 0
if (assets == 0) revert Errors.RedeemMustReceiveAssets();
// if the credit capacity delta is greater than the locked credit capacity before the state transition, revert
if (
ctx.creditCapacityBeforeRedeemUsdX18.sub(vault.getTotalCreditCapacityUsd()).lte(
ctx.lockedCreditCapacityBeforeRedeemUsdX18.intoSD59x18()
)
) {
revert Errors.NotEnoughUnlockedCreditCapacity();
}
// set withdrawal request to fulfilled
withdrawalRequest.fulfilled = true;
// emit an event
emit LogRedeem(vaultId, msg.sender, ctx.sharesMinusRedeemFeesX18.intoUint256());
}

Most vulnerable part > redeem() function

ctx.creditCapacityBeforeRedeemUsdX18 = vault.getTotalCreditCapacityUsd();
// cache the locked credit capacity before redeeming
ctx.lockedCreditCapacityBeforeRedeemUsdX18 = vault.getLockedCreditCapacityUsd();
// redeem shares previously transferred to the contract at `initiateWithdrawal` and store the returned assets
address indexToken = vault.indexToken;
uint256 assets =
IERC4626(indexToken).redeem(ctx.sharesMinusRedeemFeesX18.intoUint256(), msg.sender, address(this));
// get the redeem fee
if (ctx.sharesFees > 0) {
IERC4626(indexToken).redeem(
ctx.sharesFees, marketMakingEngineConfiguration.vaultDepositAndRedeemFeeRecipient, address(this)
);
}`

Deposit () function

```

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[](1);
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);
}

Most vulnerable part > deposit() function
```

// 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);

Impact

  • Market manipulation leading to unfair advantage for attackers.

  • Potential loss for users due to manipulated price changes.

Tools Used

  • Slither: Identified potential front-running vulnerabilities.

  • Manual Code Review: Verified transaction execution order risks.

Recommendations

  1. Implement commit-reveal schemes or minimum execution delays to mitigate front-running risks.

  2. Use off-chain signatures for transactions to prevent visibility in the mempool.

  3. Conduct MEV (Miner Extractable Value) simulations to identify exploitability.


**2. Gas Limit Issues**

Description:

Functions like deposit, redeem, and unstake perform multiple state updates and external calls, which could exceed the gas limit for a single transaction.

Code Affected:

deposit() function, redeem() function, unstake() function.

Impact

  • Increased transaction failures due to exceeding gas limits.

  • Higher costs for users performing routine operations.

Tools Used

  • Manual Code Review: Evaluated state updates and external calls.

  • Slither: Flagged inefficient gas usage patterns.

Recommendations

  1. Optimize gas usage by reducing unnecessary state updates and external calls.

  2. Batch multiple operations into a single transaction to reduce gas costs.

  3. Cache frequently accessed storage variables to minimize redundant reads.

    3 . Dependency Risks

Description

The contract relies heavily on external libraries and contracts (Vault, Collateral, Distribution). If any of these dependencies are compromised, the contract could be vulnerable.

Code Affected:

All external library imports.

Impact

  • Malicious dependencies could introduce backdoors or security exploits.

  • Unexpected changes in external contracts affecting functionality.

Tools Used

  • Manual Code Review: Checked dependency integration.

  • Slither: Analyzed risk of external contract interactions.

Recommendations

  1. Use verified and audited versions of external dependencies.

  2. Implement fallback mechanisms in case external dependencies fail.

  3. Maintain access control measures to prevent unauthorized dependency modifications.


**4. Unitialized Varibles**

Description

The contract assumes that external dependencies (Vault, Collateral) are properly initialized. If not, it could lead to unexpected behavior.

Code Affected:

All functions relying on external dependencies.

Impact

  • Unexpected contract failures due to uninitialized dependencies.

  • Potential security vulnerabilities from undefined states.

Tools Used

  • Manual Code Review: Checked variable initialization.

  • Remix: Simulated uninitialized variable scenarios.

Recommendations

  1. Enforce explicit initialization of external dependencies.

  2. Implement constructor-based dependency injection for safer setup.

  3. Add validation checks to ensure dependencies are correctly set before execution.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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