Unclaimed withdrawal requests may be incorrectly finalized
Description
When the owner of the vault batch claims withdrawal requests, the function batchClaim
determines whether to terminate the current batch claim based on the remaining available assets and the value of maxRequests
set by the owner. For available assets, the function prepareWithdrawal
is expected to return the newAvaliableAssets
as the difference between the avaliableAssets
and the withdrawal amount when the withdrawal amount does not exceed the avaliableAssets
; otherwise, if the withdrawal amount if greater than avaliableAssets
, the returned newAvaliableAssets
should be equal to avaliableAssets
.
function batchClaim(
IWithdrawalQueue withdrawalQueue,
uint256 maxRequests,
uint256 availableAssets,
// [...]
) external {
// [...]
uint256 max = lastCreatedId < lastFinalizedId + maxRequests ? lastCreatedId : lastFinalizedId + maxRequests;
for (uint256 i = lastFinalizedId + 1; i <= max;) {
uint256 newAvailiableAssets =
claimWithdrawal(i, availableAssets, withdrawalQueue, asset, strategies, parkingLot);
// slither-disable-next-line incorrect-equality
if (newAvailiableAssets == availableAssets) break;
availableAssets = newAvailiableAssets;
newLastFinalized = i;
unchecked {
i++;
}
}
// [...]
}
function claimWithdrawal(
uint256 _requestId,
uint256 avaliableAssets,
IWithdrawalQueue withdrawalQueue,
// [...]
) public returns (uint256) {
(address recipient, uint256 amount, uint256 newAvaliableAssets) =
withdrawalQueue.prepareWithdrawal(_requestId, avaliableAssets);
if (avaliableAssets != newAvaliableAssets) {
_withdrawStrategyFunds(amount, recipient, asset, strategies, parkingLot);
}
return newAvaliableAssets;
}
However, the function prepareWithdrawal
will not initialize the returned avaliableAssets
when _avaliableAssets
is less than amount
.
function prepareWithdrawal(uint256 _requestId, uint256 _avaliableAssets)
external
onlyOwner
returns (address recipient, uint256 amount, uint256 avaliableAssets)
{
// [...]
recipient = request.recipient;
WithdrawalRequest storage prevRequest = _requests[_requestId - 1];
amount = request.cumulativeAmount - prevRequest.cumulativeAmount;
if (_avaliableAssets >= amount) {
assert(_requestsByOwner[recipient].remove(_requestId));
avaliableAssets = _avaliableAssets - amount;
request.claimed = true;
// [...]
}
}
Impact
When the withdrawal amount is greater than the remaining available assets, the value of the newAvaliableAssets
returned by the function prepareWithdrawal
will be zero, which does not equal the avaliableAssets
before the call.
With Finding ref↗, because the transaction will not be reverted when totalWithdrawn < amount_
and amount_ - totalWithdrawn > float
, the function _withdrawStrategyFunds
may revert the transaction or may be successfully executed when avaliableAssets
is insufficient. If the execution succeeds, the receiver can only receive part of the assets they are supposed to receive, and the request that has not been marked as claimed in the WithdrawalQueue contract may be incorrectly recorded as the last finalized ID. As a result, the next batch claim will start processing from this request's following request. This request, which has not been fully claimed, will not be executed again.
Recommendations
Consider initializing the avaliableAssets
to the value of _avaliableAssets
in the function prepareWithdrawal
.
Remediation
This issue has been acknowledged by Blueprint Finance, and a fix was implemented in commit d66613bb↗.