Withdrawal finalization does not work
Description
The single entry point for finalizing withdrawals is the batchedFinalizeWithdrawals function, which iterates over an array of messages and calls finalizeWithdrawal on each.
Both functions have the nonReentrant modifier.
function batchedFinalizeWithdrawals(
bytes32[] calldata messages
) external nonReentrant whenNotPaused {
checkFinalizer(msg.sender);
uint64 end = uint64(messages.length);
for (uint64 idx; idx < end; idx++) {
finalizeWithdrawal(messages[idx]);
}
}
function finalizeWithdrawal(bytes32 message) private nonReentrant whenNotPaused {
require(!finalizedWithdrawals[message], "Withdrawal already finalized");
Withdrawal memory withdrawal = requestedWithdrawals[message];
checkDisputePeriod(withdrawal.requestedTime, withdrawal.requestedBlockNumber);
finalizedWithdrawals[message] = true;
usdcToken.transfer(withdrawal.user, withdrawal.usdc);
emit FinalizedWithdrawal(
FinalizedWithdrawalEvent({
user: withdrawal.user,
usdc: withdrawal.usdc,
nonce: withdrawal.nonce,
message: withdrawal.message
})
);
}Impact
Any finalization attempt will immediately revert because of the nonReentrant modifier on finalizeWithdrawal, preventing any withdrawal from the bridge from being finalized.
We classified this issue as high severity due to the fundamental importance of the finalization step for the contract operation.
Recommendations
We recommend removing the nonReentrant modifier from the private finalizeWithdrawal function and adding test cases to ensure its correct behavior.
Remediation
This issue has been acknowledged by Hyperliquid, and a fix was implemented in commit e5b7e068↗.