Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Valid

Unvalidated Locked Credit Capacity During Redemption and incorrect comparision

Summary

A vulnerability exists in the redeem function where the locked credit capacity is not validated before processing withdrawals. This allows users to withdraw funds even when the vault is insolvent (i.e., when the locked credit capacity is zero), violating the protocol's intended behavior and putting the system at risk.

Vulnerability Details

Root Cause

  • The redeem function does not check if the locked credit capacity (ctx.lockedCreditCapacityBeforeRedeemUsdX18) is non-zero before allowing withdrawals.

  • If the locked credit capacity is zero (indicating insolvency), the function proceeds with the withdrawal, even though the protocol's documentation explicitly states that funds should not be withdrawn when the vault is insolvent.

  • Although the system to check if the fund the user withdraw is more than the lockedCreditCapacity abd revert it's check if it's less

Affected Code

The issue is present in the redeem function:
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/market-making/branches/VaultRouterBranch.sol#L486

// Cache the locked credit capacity before redeeming
ctx.lockedCreditCapacityBeforeRedeemUsdX18 = vault.getLockedCreditCapacityUsd();
// No check to ensure locked credit capacity is non-zero
// Redeem shares and transfer assets
uint256 assets = IERC4626(indexToken).redeem(ctx.sharesMinusRedeemFeesX18.intoUint256(), msg.sender, address(this));

Expected Behavior

  • The protocol should prevent withdrawals when the vault is insolvent (i.e., when the locked credit capacity is zero).

https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/market-making/leaves/Vault.sol#L188

Actual Behavior

  • The protocol allows withdrawals even when the locked credit capacity is zero, violating the intended behavior and putting the system at risk.

Proof of concept

  1. Total Credit Capacity:

    TotalCreditCapacityUsd = (totalAssetInVault * adjustedPriceOfAsset) - totalDebt
    • If totalDebt exceeds (totalAssetInVault * adjustedPriceOfAsset), the result is negative, indicating the vault is insolvent.

  2. Locked Credit Capacity:

    TotalCreditCapacityLocked = TotalCreditCapacityUsd * lockedCreditRatio
    • If TotalCreditCapacityUsd is negative, the locked credit capacity will also be negative (since multiplying a negative value by a positive ratio results in a negative value).

    • However, the getLockedCreditCapacityUsd function explicitly sets the locked credit capacity to zero if TotalCreditCapacityUsd is negative:

      lockedCreditCapacityUsdX18 = creditCapacityUsdX18.lte(SD59x18_ZERO)
      ? UD60x18_ZERO
      : creditCapacityUsdX18.intoUD60x18().mul(ud60x18(self.lockedCreditRatio));

Scenario: Negative Credit Capacity

Let’s analyze the behavior when the vault is insolvent (TotalCreditCapacityUsd is negative):

  1. Initial State:

    • TotalCreditCapacityUsd = -1000 (vault is insolvent).

    • TotalCreditCapacityLocked = 0 (since TotalCreditCapacityUsd is negative).

  2. User Withdraws Funds:

    • Suppose the user withdraws assets worth 500.

    • The new TotalCreditCapacityUsd becomes:

      TotalCreditCapacityUsd = -1000 - 500 = -1500
    • The new TotalCreditCapacityLocked remains zero (since TotalCreditCapacityUsd is still negative).

  3. Credit Capacity Delta Check:

    • The redeem function checks if the credit capacity delta is greater than the locked credit capacity:

      creditCapacityDelta = creditCapacityBeforeRedeemUsdX18 - creditCapacityAfterRedeemUsdX18
    • In this case:

      creditCapacityDelta = (-1000) - (-1500) = 500
    • The locked credit capacity is zero, so the check:

      if (
      ctx.creditCapacityBeforeRedeemUsdX18.sub(vault.getTotalCreditCapacityUsd()).lte(
      ctx.lockedCreditCapacityBeforeRedeemUsdX18.intoSD59x18()
      )
      ) {
      revert Errors.NotEnoughUnlockedCreditCapacity();
      }

      becomes:

      500 <= 0
    • This check fails, and the function does not revert with Errors.NotEnoughUnlockedCreditCapacity().

Impact

Users can withdraw funds even when the vault is insolvent, leading to liquidity issues or insolvency for connected markets.

Tools Used

The vulnerability was identified through a detailed review of the redeem function and its interaction with the getLockedCreditCapacityUsd function.

Recommendations

Add a check to ensure the locked credit capacity is non-zero before allowing withdrawals:

// Revert if the vault is insolvent (locked credit capacity is zero)
if (ctx.lockedCreditCapacityBeforeRedeemUsdX18 == UD60x18_ZERO) {
revert Errors.VaultInsolvent();
}

Modify the check to revert when the delta is greater than lockedCreditCapacity

if (
@> ctx.creditCapacityBeforeRedeemUsdX18.sub(vault.getTotalCreditCapacityUsd()).gte(
ctx.lockedCreditCapacityBeforeRedeemUsdX18.intoSD59x18()
)
) {
revert Errors.NotEnoughUnlockedCreditCapacity();
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

The check in VaultRouterBranch::redeem should be comparing remaining capacity against required locked capacity not delta against locked capacity

Support

FAQs

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