All migrated withdrarwals that require more than 135,175 gas may be bricked
Migrated withdrawals are given an "outer" (Portal) gas limit of `calldata cost + 200,000`, and an "inner" (CrossDomainMessenger) gas limit of `0`. The assumption is that the CrossDomainMessenger is replayable, so there is no need to specify a correct gas limit.\\nThis is an incorect assumption. For any withdrawals that require more than 135,175 gas, insufficient gas can be sent such that CrossDomainMessenger's external call reverts and the remaining 1/64th of the gas sent is not enough for replayability to be encoded in the Cross Domain Messenger.\\nHowever, the remaining 1/64th of gas in the Portal is sufficient to have the transaction finalize, so that the Portal will not process the withdrawal again.\\nWhen old withdrawals are migrated to Bedrock, they are encoded as calls to `L1CrossDomainMessenger.relayMessage()` as follows:\\n```\\nfunc MigrateWithdrawal(withdrawal *LegacyWithdrawal, l1CrossDomainMessenger *common.Address) (*Withdrawal, error) {\\n // Attempt to parse the value\\n value, err := withdrawal.Value()\\n if err != nil {\\n  return nil, fmt.Errorf("cannot migrate withdrawal: %w", err)\\n }\\n\\n abi, err := bindings.L1CrossDomainMessengerMetaData.GetAbi()\\n if err != nil {\\n  return nil, err\\n }\\n\\n // Migrated withdrawals are specified as version 0. Both the\\n // L2ToL1MessagePasser and the CrossDomainMessenger use the same\\n // versioning scheme. Both should be set to version 0\\n versionedNonce := EncodeVersionedNonce(withdrawal.XDomainNonce, new(big.Int))\\n // Encode the call to `relayMessage` on the `CrossDomainMessenger`.\\n // The minGasLimit can safely be 0 here.\\n data, err := abi.Pack(\\n  "relayMessage",\\n  versionedNonce,\\n  withdrawal.XDomainSender,\\n  withdrawal.XDomainTarget,\\n  value,\\n  new(big.Int), // <= THIS IS THE INNER GAS LIMIT BEING SET TO ZERO\\n  []byte(withdrawal.XDomainData),\\n )\\n if err != nil {\\n  return nil, fmt.Errorf("cannot abi encode relayMessage: %w", err)\\n }\\n\\n gasLimit := MigrateWithdrawalGasLimit(data)\\n\\n w := NewWithdrawal(\\n  versionedNonce,\\n  &predeploys.L2CrossDomainMessengerAddr,\\n  l1CrossDomainMessenger,\\n  value,\\n  new(big.Int).SetUint64(gasLimit), // <= THIS IS THE OUTER GAS LIMIT BEING SET\\n  data,\\n )\\n return w, nil\\n}\\n```\\n\\nAs we can see, the `relayMessage()` call uses a gasLimit of zero (see comments above), while the outer gas limit is calculated by the `MigrateWithdrawalGasLimit()` function:\\n```\\nfunc MigrateWithdrawalGasLimit(data []byte) uint64 {\\n // Compute the cost of the calldata\\n dataCost := uint64(0)\\n for _, b := range data {\\n  if b == 0 {\\n   dataCost += params.TxDataZeroGas\\n  } else {\\n   dataCost += params.TxDataNonZeroGasEIP2028\\n  }\\n }\\n\\n // Set the outer gas limit. This cannot be zero\\n gasLimit := dataCost + 200_000\\n // Cap the gas limit to be 25 million to prevent creating withdrawals\\n // that go over the block gas limit.\\n if gasLimit > 25_000_000 {\\n  gasLimit = 25_000_000\\n }\\n\\n return gasLimit\\n}\\n```\\n\\nThis calculates the outer gas limit value by adding the calldata cost to 200,000.\\nLet's move over to the scenario in which these values are used to see why they can cause a problem.\\nWhen a transaction is proven, we can call `OptimismPortal.finalizeWithdrawalTransaction()` to execute the transaction. In the case of migrated withdrawals, this executes the following flow:\\n`OptimismPortal` calls to `L1CrossDomainMessenger` with a gas limit of `200,000 + calldata`\\nThis guarantees remaining gas for continued execution after the call of `(200_000 + calldata) * 64/63 * 1/64 > 3174`\\nXDM uses `41,002` gas before making the call, leaving `158,998` remaining for the call\\nThe `SafeCall.callWithMinGas()` succeeds, since the inner gas limit is set to 0\\nIf the call uses up all of the avaialble gas (succeeding or reverting), we are left with `158,998` * 1/64 = 2,484 for the remaining execution\\nThe remaining execution includes multiple SSTOREs which totals `23,823` gas, resulting in an OutOfGas revert\\nIn fact, if the call uses any amount greater than `135,175`, we will have less than `23,823` gas remaining and will revert\\nAs a result, none of the updates to `L1CrossDomainMessenger` occur, and the transaction is not marked in `failedMessages` for replayability\\nHowever, the remaining `3174` gas is sufficient to complete the transction on the `OptimismPortal`, which sets `finalizedWithdrawals[hash] = true` and locks the withdrawals from ever being made again
There doesn't seem to be an easy fix for this, except to adjust the migration process so that migrated withdrawals are directly saved as `failedMessages` on the `L1CrossDomainMessenger` (and marked as `finalizedWithdrawals` on the OptimismPortal), rather than needing to be reproven through the normal flow.
Any migrated withdrawal that uses more than `135,175` gas will be bricked if insufficient gas is sent. This could be done by a malicious attacker bricking thousands of pending withdrawals or, more likely, could happen to users who accidentally executed their withdrawal with too little gas and ended up losing it permanently.
```\\nfunc MigrateWithdrawal(withdrawal *LegacyWithdrawal, l1CrossDomainMessenger *common.Address) (*Withdrawal, error) {\\n // Attempt to parse the value\\n value, err := withdrawal.Value()\\n if err != nil {\\n  return nil, fmt.Errorf("cannot migrate withdrawal: %w", err)\\n }\\n\\n abi, err := bindings.L1CrossDomainMessengerMetaData.GetAbi()\\n if err != nil {\\n  return nil, err\\n }\\n\\n // Migrated withdrawals are specified as version 0. Both the\\n // L2ToL1MessagePasser and the CrossDomainMessenger use the same\\n // versioning scheme. Both should be set to version 0\\n versionedNonce := EncodeVersionedNonce(withdrawal.XDomainNonce, new(big.Int))\\n // Encode the call to `relayMessage` on the `CrossDomainMessenger`.\\n // The minGasLimit can safely be 0 here.\\n data, err := abi.Pack(\\n  "relayMessage",\\n  versionedNonce,\\n  withdrawal.XDomainSender,\\n  withdrawal.XDomainTarget,\\n  value,\\n  new(big.Int), // <= THIS IS THE INNER GAS LIMIT BEING SET TO ZERO\\n  []byte(withdrawal.XDomainData),\\n )\\n if err != nil {\\n  return nil, fmt.Errorf("cannot abi encode relayMessage: %w", err)\\n }\\n\\n gasLimit := MigrateWithdrawalGasLimit(data)\\n\\n w := NewWithdrawal(\\n  versionedNonce,\\n  &predeploys.L2CrossDomainMessengerAddr,\\n  l1CrossDomainMessenger,\\n  value,\\n  new(big.Int).SetUint64(gasLimit), // <= THIS IS THE OUTER GAS LIMIT BEING SET\\n  data,\\n )\\n return w, nil\\n}\\n```\\n