Part 2

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

Wrong event emission in VaultRouterBranch::redeem()

Summary

The VaultRouterBranch::redeem() function is designed to emit an event LogRedeem(uint256 indexed vaultId, address indexed user, uint256 shares) when a user redeems shares from the vault. However, the function incorrectly emits the shares amount after deducting fees, instead of the original number of shares the user redeemed.

Vulnerability Details

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();
console.log("after", vault.getTotalCreditCapacityUsd().intoUint256());
// 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());
}

The LogRedeem event should accurately reflect the total number of shares the user requested to redeem. Instead, it emits a lower number (after fees are deducted). The function calculates fees and subtracts them from the user’s redeemed shares. Instead of emitting the full redeemed amount, the event logs the shares after fees are deducted. This can lead to incorrect event tracking and on-chain data inconsistencies.

Impact

Off-chain systems tracking vault shares via events will record incorrect values, leading to misleading accounting.

Tools Used

Manual Review

Recommendations

Ensure that LogRedeem emits the original number of shares before fee deductions

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();
console.log("after", vault.getTotalCreditCapacityUsd().intoUint256());
// 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.shares);
- emit LogRedeem(vaultId, msg.sender, ctx.sharesMinusRedeemFeesX18.intoUint256());
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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