Summary
The return value retFinalizedWithdrawals in the WithdrawalPool::getFinalizedWithdrawalIdsByOwner() function is incorrect, potentially leading to incomplete data being returned.
Vulnerability Details
The issue lies within the logic that handles the finalization of withdrawals, specifically in the way the function iterates over and returns finalized withdrawals. The problematic parts of the code are marked with @> in the following analysis:
The length of finalizedWithdrawals is equal to withdrawalIds.length.
The counter totalFinalizedWithdrawals tracks the number of valid finalized withdrawals, ensuring totalFinalizedWithdrawals <= finalizedWithdrawals.length. However, totalFinalizedWithdrawals should only be used to initialize the retFinalizedWithdrawals array, not for iterating over finalizedWithdrawals.
To correctly traverse the finalizedWithdrawals array and populate retFinalizedWithdrawals, the loop should use finalizedWithdrawals.length instead of totalFinalizedWithdrawals.
function getFinalizedWithdrawalIdsByOwner(
address _account
) external view returns (uint256[] memory, uint256) {
uint256[] memory withdrawalIds = getWithdrawalIdsByOwner(_account);
uint256[] memory batchIds = getBatchIds(withdrawalIds);
@> uint256[] memory finalizedWithdrawals = new uint256[]();
uint256 totalFinalizedWithdrawals;
uint256 totalWithdrawable;
for (uint256 i = 0; i < batchIds.length; ++i) {
Withdrawal memory withdrawal = queuedWithdrawals[withdrawalIds[i]];
if (batchIds[i] != 0 || withdrawal.partiallyWithdrawableAmount != 0) {
finalizedWithdrawals[i] = withdrawalIds[i];
@> totalFinalizedWithdrawals++;
totalWithdrawable += withdrawal.partiallyWithdrawableAmount;
if (batchIds[i] != 0) {
totalWithdrawable +=
(uint256(withdrawalBatches[batchIds[i]].stakePerShares) *
uint256(withdrawal.sharesRemaining)) /
1e18;
}
} else {
break;
}
}
uint256[] memory retFinalizedWithdrawals = new uint256[]();
uint256 withdrawalsAdded;
@> for (uint256 i = 0; i < totalFinalizedWithdrawals; ++i) {
@> uint256 withdrawalId = finalizedWithdrawals[i];
if (withdrawalId != 0) {
retFinalizedWithdrawals[withdrawalsAdded] = withdrawalId;
withdrawalsAdded++;
}
}
return (retFinalizedWithdrawals, totalWithdrawable);
}
Code Snippet
https://github.com/Cyfrin/2024-09-stakelink/blob/f5824f9ad67058b24a2c08494e51ddd7efdbb90b/contracts/core/priorityPool/WithdrawalPool.sol#L213-L253
Impact
The incorrect use of totalFinalizedWithdrawals in the iteration results in incomplete data being returned in retFinalizedWithdrawals. This could lead to users receiving incomplete information about their finalized withdrawals, causing issues with fund visibility or tracking.
Tools Used
Manual Review
Recommendations
To fix this issue, the loop responsible for populating retFinalizedWithdrawals should iterate over the entire finalizedWithdrawals array, not just up to totalFinalizedWithdrawals, to ensure that all potential entries are processed.
function getFinalizedWithdrawalIdsByOwner(
address _account
) external view returns (uint256[] memory, uint256) {
uint256[] memory withdrawalIds = getWithdrawalIdsByOwner(_account);
uint256[] memory batchIds = getBatchIds(withdrawalIds);
uint256[] memory finalizedWithdrawals = new uint256[]();
uint256 totalFinalizedWithdrawals;
uint256 totalWithdrawable;
for (uint256 i = 0; i < batchIds.length; ++i) {
Withdrawal memory withdrawal = queuedWithdrawals[withdrawalIds[i]];
if (batchIds[i] != 0 || withdrawal.partiallyWithdrawableAmount != 0) {
finalizedWithdrawals[i] = withdrawalIds[i];
totalFinalizedWithdrawals++;
totalWithdrawable += withdrawal.partiallyWithdrawableAmount;
if (batchIds[i] != 0) {
totalWithdrawable +=
(uint256(withdrawalBatches[batchIds[i]].stakePerShares) *
uint256(withdrawal.sharesRemaining)) /
1e18;
}
} else {
break;
}
}
uint256[] memory retFinalizedWithdrawals = new uint256[]();
uint256 withdrawalsAdded;
- for (uint256 i = 0; i < totalFinalizedWithdrawals; ++i) {
+ for (uint256 i = 0; i < finalizedWithdrawals.length; ++i) {
uint256 withdrawalId = finalizedWithdrawals[i];
if (withdrawalId != 0) {
retFinalizedWithdrawals[withdrawalsAdded] = withdrawalId;
withdrawalsAdded++;
}
}
return (retFinalizedWithdrawals, totalWithdrawable);
}