Part 2

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

Users can withdraw below the locked credity capacity bypassing the locked ratio check

Summary

According to the current implementation some percentage of assets should within the contract to prevent the credit system from being insolvent this amounts are restricted by a check after every redemption but this check doesn't prevent this has users will be able to withdraw all the asset without leaving anything.

Vulnerability Details

Lockedcredit ratio is used to calculate the amount of assets that should still remain in the vault to secure the delegation system but the check implemented during redemption will not handle this correctly.

@audit>>> /// @param lockedCreditRatio The ratio that determines how much of the vault's total assets can't be
/// withdrawn according to the Vault's total debt, in order to secure the credit delegation system.
// 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();
@audit>>> // if the credit capacity delta is greater than the locked credit capacity before the state transition, revert
@audit>>> 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());
}

Before redemption , Credit capacity = $ 1000

User withdraws shares worth = $ 995

Using a locked ratio of 2% = 1000 * 2 % = $ 20

Afteer withdrawing we should not be able to go less than the locked Credit but instead we do this

New Credit Capacity = $ 5

Check 1000 - 5 < 20

995 < 20

will pass but the remain credit capacity is actually 5 and it is less than the amount that should remain locked 20 .

FOR a user withdrawing just 1 dollar the current check will revert his redemption even though the capacity is 999 dollars and higher than 20 dollars.

The check DOS a user when the vault is healthy and doesn't prevent users from redeeming assets that will send the capacity below the locked ratio.

Instead we should be checking

e.g 1 . After withdrawal is the new capacity below the former locked

5 USD below 20 USD => revert

For the smaller amount withdrawn also

999 USD below 20 USD => allow withdrawal

@audit>> /// @notice Returns the vault's minimum credit capacity allocated to the connected markets.
@audit>> /// @dev Prevents the vault's LPs from withdrawing more collateral than allowed, leading to potential liquidity
@audit>> /// issues to connected markets.
/// @dev If the credit capacity goes to zero or below, meaning the vault is insolvent, the locked capacity will be
/// zero, so functions using this method must ensure funds can't be withdrawn in that state.
/// @param self The vault storage pointer.
/// @return lockedCreditCapacityUsdX18 The vault's minimum credit capacity in USD.
function getLockedCreditCapacityUsd(Data storage self)
internal
view
returns (UD60x18 lockedCreditCapacityUsdX18)
{
@audit>> SD59x18 creditCapacityUsdX18 = getTotalCreditCapacityUsd(self);
lockedCreditCapacityUsdX18 = creditCapacityUsdX18.lte(SD59x18_ZERO)
? UD60x18_ZERO
@audit>> : creditCapacityUsdX18.intoUD60x18().mul(ud60x18(self.lockedCreditRatio));
}

Impact

Bypass check that can lead to vault being unable to maintain the credit delegation system and DOS when users are withdrawing just a fraction with the vault still solvent.

Tools Used

Manual Review

Recommendations

Following the comment

// if the credit capacity delta is greater than the locked credit capacity before the state transition, revert

Change the Check. check the Present vault credit capacity and revert if less than the locked ratio before making this call.

Updates

Lead Judging Commences

inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 6 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.