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↗.