Part 2

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

Incorrect Credit Capacity Check in VaultRouterBranch::redeem() Function

Summary

The redeem function is designed to allow users to redeem index tokens for collateral assets from the vault. The vault enforces a locked capacity, meaning that after a user withdrawal, the remaining vault balance should never fall below this locked capacity.

However, the function incorrectly implements the locked capacity check:

  • It checks if the difference between the credit capacity before and after redemption is ≤ locked capacity.

  • This check is flawed because it allows users to fully withdraw their deposits without triggering a revert.

Vulnerability Details

The function should ensure that vault balance after withdrawal remains above the locked capacity.

  • Instead, it only checks the difference between pre- and post-redeem credit capacity, which does not guarantee that the vault maintains its required balance.

  • This means users can withdraw all their funds, bypassing the locked capacity restriction.

POC

function test_Redeem(
) external
{
uint128 vaultId = 8;
uint128 assetsToDeposit = 2 * 10 ** 6;
uint128 sharesToWithdraw = 1.98 * 10 ** 6;
// ensure valid vault and load vault config
//vaultId = uint128(bound(vaultId, INITIAL_VAULT_ID, FINAL_VAULT_ID));
VaultConfig memory fuzzVaultConfig = getFuzzVaultConfig(vaultId);
Vault.UpdateParams memory params = Vault.UpdateParams({
vaultId: fuzzVaultConfig.vaultId,
depositCap: fuzzVaultConfig.depositCap,
withdrawalDelay: fuzzVaultConfig.withdrawalDelay,
isLive: true,
lockedCreditRatio: 0.5e18
});
changePrank({ msgSender: users.owner.account });
marketMakingEngine.updateVaultConfiguration(params);
// ensure valid deposit amount
address user = users.naruto.account;
// peform the deposit
fundUserAndDepositInVault(user, vaultId, assetsToDeposit);
uint128 userVaultShares = uint128(IERC20(fuzzVaultConfig.indexToken).balanceOf(user));
// intiate the withdrawal
vm.startPrank(user);
marketMakingEngine.initiateWithdrawal(vaultId, sharesToWithdraw);
// fast forward block.timestamp to after withdraw delay has passed
skip(fuzzVaultConfig.withdrawalDelay + 1);
UD60x18 expectedAssetsX18 = marketMakingEngine.getIndexTokenSwapRate(vaultId, sharesToWithdraw, false);
uint256 redeemFee = vaultsConfig[vaultId].redeemFee;
UD60x18 expectedAssetsMinusRedeemFeeX18 = expectedAssetsX18.sub(expectedAssetsX18.mul(ud60x18(redeemFee)));
UD60x18 sharesMinusRedeemFeesX18 =
marketMakingEngine.getVaultAssetSwapRate(vaultId, expectedAssetsMinusRedeemFeeX18.intoUint256(), false);
// save and verify pre state
RedeemState memory pre = _getRedeemState(
user, users.vaultFeeRecipient.account, IERC20(fuzzVaultConfig.asset), IERC20(fuzzVaultConfig.indexToken)
);
// perform the redemption
vm.expectEmit();
emit VaultRouterBranch.LogRedeem(vaultId, user, sharesMinusRedeemFeesX18.intoUint256());
marketMakingEngine.redeem(vaultId, WITHDRAW_REQUEST_ID, 0);
// save and verify post state
RedeemState memory post = _getRedeemState(
user, users.vaultFeeRecipient.account, IERC20(fuzzVaultConfig.asset), IERC20(fuzzVaultConfig.indexToken)
);
// verify withdrawal request marked as fulfilled
WithdrawalRequest.Data memory withdrawalRequest =
marketMakingEngine.exposed_WithdrawalRequest_loadExisting(vaultId, user, WITHDRAW_REQUEST_ID);
assertTrue(withdrawalRequest.fulfilled);
// verify redeem fees paid to the vault redeem fee recipient
assertEq(
post.feeReceiverAssetBal - pre.feeReceiverAssetBal,
expectedAssetsX18.mul(ud60x18(redeemFee)).intoUint256(),
"Redeem fees paid to FeeReceiver"
);
assertEq(post.redeemerVaultBal, userVaultShares - sharesToWithdraw, "Shares deducted from Redeemer");
assertEq(post.marketEngineVaultBal, 0, "No shares stuck in market engine");
assertEq(
post.redeemerAssetBal,
expectedAssetsMinusRedeemFeeX18.intoUint256(),
"Redeemer received correct asset tokens"
);
assertEq(
post.redeemerAssetBal + post.feeReceiverAssetBal + post.vaultAssetBal,
assetsToDeposit,
"All deposited assets accounted"
);
// Deposited and withdraw all shares
}

Impact

Users can bypass the locked capacity of the vault

Tools Used

Manual Review

Recommendations

Instead of comparing the difference between credit capacities, check that the vault balance after redeeming remains greater than or equal to the locked capacity.

require(
vaultBalanceAfterRedeem >= lockedCapacity,
"Vault balance must remain above locked capacity"
);
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.