Assessment reports>Hyperliquid>High findings>Withdrawal finalization does not work
Category: Coding Mistakes

Withdrawal finalization does not work

High Severity
High Impact
High Likelihood

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.

Zellic © 2025Back to top ↑