# Causing users lose their fund during finalizing withdrawal transaction

Attacker replays a failed L2 message to reenter OptimismPortal.finalizeWithdrawalTransaction on a victim's withdrawal, whose inner relayMessage reverts on the reentrancy guard, marking the withdrawal finalized while funds are never released.

- Payout: Contest reward
- Severity: high
- Program: Optimism
- Credited: HE1M
- Disclosed: 2023-01-19
- Write-up quality: 8/10
- Original: https://github.com/sherlock-audit/2023-01-optimism-judging/issues/87
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-01-optimism%3AH-3
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:business-logic vuln:reentrancy

**Target:** OptimismPortal / L1CrossDomainMessenger withdrawal finalization
**Root cause:** OptimismPortal.finalizeWithdrawalTransaction marks the withdrawal finalized even when the inner L1CrossDomainMessenger.relayMessage call fails; the reentrancy guard on relayMessage makes that inner call revert when finalization is triggered from within another relayMessage, and the failure is swallowed.
**Steps:**
1. Deploy AttackContract on L1 with attack() that reverts while donotRevert=false, else calls optimismPortal.call(metaData).
2. From L2 call sendMessage(_target=AttackContract, _message=abi.encodeWithSignature("attack()"), _minGasLimit) so the relay on L1 reverts and failedMessages[versionedHash]=true.
3. Call enableRevert() to set donotRevert=true.
4. Wait for victim's withdrawal to be proved; call setMetaData(victimTx) storing finalizeWithdrawalTransaction.selector+victimTx.
5. After challenge period, before victim finalizes, call L1CrossDomainMessenger.relayMessage to replay the failed message.
6. attack() now executes optimismPortal.finalizeWithdrawalTransaction(victimTx); inner relayMessage reverts on ReentrancyGuard, but finalizeWithdrawalTransaction still marks the withdrawal finalized.
7. Victim's withdrawal is flagged finalized but funds never released; attacker's relay succeeds.
**How found:** Researcher read OptimismPortal.finalizeWithdrawalTransaction and noticed the inner relayMessage call is wrapped in try/catch that ignores failures, combined with the reentrancy guard on CrossDomainMessenger.relayMessage; hypothesized that triggering finalization from inside another relayMessage would make the inner call revert while the outer finalization still succeeds.
**Exploitation:** Attacker needs only a small L2->L1 message and gas; no capital. By pre-staging a failed message and toggling the contract, they force a victim's finalizeWithdrawalTransaction to run inside their own relayMessage, so the victim's inner relayMessage hits the reentrancy guard and reverts silently, permanently bricking the victim's withdrawal while the portal records it as finalized.
**Insight:** When a contract swallows inner-call failures (try/catch without revert) and the inner callee has a reentrancy guard, look for a path to invoke the outer function from inside the guarded function so the guard itself becomes the failure trigger; pre-staging a retryable failed message gives a controllable re-entry point.
