github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/src/L1/OptimismPortal2.sol (about)

     1  // SPDX-License-Identifier: MIT
     2  pragma solidity 0.8.15;
     3  
     4  import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
     5  import { SafeCall } from "src/libraries/SafeCall.sol";
     6  import { DisputeGameFactory, IDisputeGame } from "src/dispute/DisputeGameFactory.sol";
     7  import { SystemConfig } from "src/L1/SystemConfig.sol";
     8  import { SuperchainConfig } from "src/L1/SuperchainConfig.sol";
     9  import { Constants } from "src/libraries/Constants.sol";
    10  import { Types } from "src/libraries/Types.sol";
    11  import { Hashing } from "src/libraries/Hashing.sol";
    12  import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol";
    13  import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol";
    14  import { ResourceMetering } from "src/L1/ResourceMetering.sol";
    15  import { ISemver } from "src/universal/ISemver.sol";
    16  import { Constants } from "src/libraries/Constants.sol";
    17  
    18  import "src/libraries/DisputeTypes.sol";
    19  
    20  /// @custom:proxied
    21  /// @title OptimismPortal2
    22  /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1
    23  ///         and L2. Messages sent directly to the OptimismPortal have no form of replayability.
    24  ///         Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface.
    25  contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
    26      /// @notice Represents a proven withdrawal.
    27      /// @custom:field disputeGameProxy The address of the dispute game proxy that the withdrawal was proven against.
    28      /// @custom:field timestamp        Timestamp at whcih the withdrawal was proven.
    29      struct ProvenWithdrawal {
    30          IDisputeGame disputeGameProxy;
    31          uint64 timestamp;
    32      }
    33  
    34      /// @notice The delay between when a withdrawal transaction is proven and when it may be finalized.
    35      uint256 internal immutable PROOF_MATURITY_DELAY_SECONDS;
    36  
    37      /// @notice The delay between when a dispute game is resolved and when a withdrawal proven against it may be
    38      ///         finalized.
    39      uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS;
    40  
    41      /// @notice Version of the deposit event.
    42      uint256 internal constant DEPOSIT_VERSION = 0;
    43  
    44      /// @notice The L2 gas limit set when eth is deposited using the receive() function.
    45      uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000;
    46  
    47      /// @notice Address of the L2 account which initiated a withdrawal in this transaction.
    48      ///         If the of this variable is the default L2 sender address, then we are NOT inside of
    49      ///         a call to finalizeWithdrawalTransaction.
    50      address public l2Sender;
    51  
    52      /// @notice A list of withdrawal hashes which have been successfully finalized.
    53      mapping(bytes32 => bool) public finalizedWithdrawals;
    54  
    55      /// @custom:legacy
    56      /// @custom:spacer provenWithdrawals
    57      /// @notice Spacer taking up the legacy `provenWithdrawals` mapping slot.
    58      bytes32 private spacer_52_0_32;
    59  
    60      /// @custom:legacy
    61      /// @custom:spacer paused
    62      /// @notice Spacer for backwards compatibility.
    63      bool private spacer_53_0_1;
    64  
    65      /// @notice Contract of the Superchain Config.
    66      SuperchainConfig public superchainConfig;
    67  
    68      /// @custom:legacy
    69      /// @custom:spacer l2Oracle
    70      /// @notice Spacer taking up the legacy `l2Oracle` address slot.
    71      address private spacer_54_0_20;
    72  
    73      /// @notice Contract of the SystemConfig.
    74      /// @custom:network-specific
    75      SystemConfig public systemConfig;
    76  
    77      /// @notice Address of the DisputeGameFactory.
    78      /// @custom:network-specific
    79      DisputeGameFactory public disputeGameFactory;
    80  
    81      /// @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data.
    82      mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals;
    83  
    84      /// @notice A mapping of dispute game addresses to whether or not they are blacklisted.
    85      mapping(IDisputeGame => bool) public disputeGameBlacklist;
    86  
    87      /// @notice The game type that the OptimismPortal consults for output proposals.
    88      GameType public respectedGameType;
    89  
    90      /// @notice The timestamp at which the respected game type was last updated.
    91      uint64 public respectedGameTypeUpdatedAt;
    92  
    93      /// @notice Emitted when a transaction is deposited from L1 to L2.
    94      ///         The parameters of this event are read by the rollup node and used to derive deposit
    95      ///         transactions on L2.
    96      /// @param from       Address that triggered the deposit transaction.
    97      /// @param to         Address that the deposit transaction is directed to.
    98      /// @param version    Version of this deposit transaction event.
    99      /// @param opaqueData ABI encoded deposit data to be parsed off-chain.
   100      event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData);
   101  
   102      /// @notice Emitted when a withdrawal transaction is proven.
   103      /// @param withdrawalHash Hash of the withdrawal transaction.
   104      /// @param from           Address that triggered the withdrawal transaction.
   105      /// @param to             Address that the withdrawal transaction is directed to.
   106      event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to);
   107  
   108      /// @notice Emitted when a withdrawal transaction is finalized.
   109      /// @param withdrawalHash Hash of the withdrawal transaction.
   110      /// @param success        Whether the withdrawal transaction was successful.
   111      event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success);
   112  
   113      /// @notice Reverts when paused.
   114      modifier whenNotPaused() {
   115          require(!paused(), "OptimismPortal: paused");
   116          _;
   117      }
   118  
   119      /// @notice Semantic version.
   120      /// @custom:semver 3.3.0
   121      string public constant version = "3.3.0";
   122  
   123      /// @notice Constructs the OptimismPortal contract.
   124      constructor(
   125          uint256 _proofMaturityDelaySeconds,
   126          uint256 _disputeGameFinalityDelaySeconds,
   127          GameType _initialRespectedGameType
   128      ) {
   129          PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds;
   130          DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds;
   131          respectedGameType = _initialRespectedGameType;
   132  
   133          initialize({
   134              _disputeGameFactory: DisputeGameFactory(address(0)),
   135              _systemConfig: SystemConfig(address(0)),
   136              _superchainConfig: SuperchainConfig(address(0))
   137          });
   138      }
   139  
   140      /// @notice Initializer.
   141      /// @param _disputeGameFactory Contract of the DisputeGameFactory.
   142      /// @param _systemConfig Contract of the SystemConfig.
   143      /// @param _superchainConfig Contract of the SuperchainConfig.
   144      function initialize(
   145          DisputeGameFactory _disputeGameFactory,
   146          SystemConfig _systemConfig,
   147          SuperchainConfig _superchainConfig
   148      )
   149          public
   150          initializer
   151      {
   152          disputeGameFactory = _disputeGameFactory;
   153          systemConfig = _systemConfig;
   154          superchainConfig = _superchainConfig;
   155          if (l2Sender == address(0)) {
   156              l2Sender = Constants.DEFAULT_L2_SENDER;
   157          }
   158          __ResourceMetering_init();
   159      }
   160  
   161      /// @notice Getter function for the contract of the SystemConfig on this chain.
   162      ///         Public getter is legacy and will be removed in the future. Use `systemConfig()` instead.
   163      /// @return Contract of the SystemConfig on this chain.
   164      /// @custom:legacy
   165      function SYSTEM_CONFIG() external view returns (SystemConfig) {
   166          return systemConfig;
   167      }
   168  
   169      /// @notice Getter function for the address of the guardian.
   170      ///         Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead.
   171      /// @return Address of the guardian.
   172      /// @custom:legacy
   173      function GUARDIAN() external view returns (address) {
   174          return guardian();
   175      }
   176  
   177      /// @notice Getter function for the address of the guardian.
   178      ///         Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead.
   179      /// @return Address of the guardian.
   180      /// @custom:legacy
   181      function guardian() public view returns (address) {
   182          return superchainConfig.guardian();
   183      }
   184  
   185      /// @notice Getter for the current paused status.
   186      function paused() public view returns (bool) {
   187          return superchainConfig.paused();
   188      }
   189  
   190      /// @notice Getter for the proof maturity delay.
   191      function proofMaturityDelaySeconds() public view returns (uint256) {
   192          return PROOF_MATURITY_DELAY_SECONDS;
   193      }
   194  
   195      /// @notice Getter for the dispute game finality delay.
   196      function disputeGameFinalityDelaySeconds() public view returns (uint256) {
   197          return DISPUTE_GAME_FINALITY_DELAY_SECONDS;
   198      }
   199  
   200      /// @notice Computes the minimum gas limit for a deposit.
   201      ///         The minimum gas limit linearly increases based on the size of the calldata.
   202      ///         This is to prevent users from creating L2 resource usage without paying for it.
   203      ///         This function can be used when interacting with the portal to ensure forwards
   204      ///         compatibility.
   205      /// @param _byteCount Number of bytes in the calldata.
   206      /// @return The minimum gas limit for a deposit.
   207      function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) {
   208          return _byteCount * 16 + 21000;
   209      }
   210  
   211      /// @notice Accepts value so that users can send ETH directly to this contract and have the
   212      ///         funds be deposited to their address on L2. This is intended as a convenience
   213      ///         function for EOAs. Contracts should call the depositTransaction() function directly
   214      ///         otherwise any deposited funds will be lost due to address aliasing.
   215      receive() external payable {
   216          depositTransaction(msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes(""));
   217      }
   218  
   219      /// @notice Accepts ETH value without triggering a deposit to L2.
   220      ///         This function mainly exists for the sake of the migration between the legacy
   221      ///         Optimism system and Bedrock.
   222      function donateETH() external payable {
   223          // Intentionally empty.
   224      }
   225  
   226      /// @notice Getter for the resource config.
   227      ///         Used internally by the ResourceMetering contract.
   228      ///         The SystemConfig is the source of truth for the resource config.
   229      /// @return ResourceMetering ResourceConfig
   230      function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory) {
   231          return systemConfig.resourceConfig();
   232      }
   233  
   234      /// @notice Proves a withdrawal transaction.
   235      /// @param _tx               Withdrawal transaction to finalize.
   236      /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against.
   237      /// @param _outputRootProof  Inclusion proof of the L2ToL1MessagePasser contract's storage root.
   238      /// @param _withdrawalProof  Inclusion proof of the withdrawal in L2ToL1MessagePasser contract.
   239      function proveWithdrawalTransaction(
   240          Types.WithdrawalTransaction memory _tx,
   241          uint256 _disputeGameIndex,
   242          Types.OutputRootProof calldata _outputRootProof,
   243          bytes[] calldata _withdrawalProof
   244      )
   245          external
   246          whenNotPaused
   247      {
   248          // Prevent users from creating a deposit transaction where this address is the message
   249          // sender on L2. Because this is checked here, we do not need to check again in
   250          // `finalizeWithdrawalTransaction`.
   251          require(_tx.target != address(this), "OptimismPortal: you cannot send messages to the portal contract");
   252  
   253          // Fetch the dispute game proxy from the `DisputeGameFactory` contract.
   254          (GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex);
   255          Claim outputRoot = gameProxy.rootClaim();
   256  
   257          // The game type of the dispute game must be the respected game type.
   258          require(gameType.raw() == respectedGameType.raw(), "OptimismPortal: invalid game type");
   259  
   260          // Verify that the output root can be generated with the elements in the proof.
   261          require(
   262              outputRoot.raw() == Hashing.hashOutputRootProof(_outputRootProof),
   263              "OptimismPortal: invalid output root proof"
   264          );
   265  
   266          // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier.
   267          bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);
   268          ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash];
   269  
   270          // We generally want to prevent users from proving the same withdrawal multiple times
   271          // because each successive proof will update the timestamp. A malicious user can take
   272          // advantage of this to prevent other users from finalizing their withdrawal. However,
   273          // in the case that an honest user proves their withdrawal against a dispute game that
   274          // resolves against the root claim, or the dispute game is blacklisted, we allow
   275          // re-proving the withdrawal against a new proposal.
   276          IDisputeGame oldGame = provenWithdrawal.disputeGameProxy;
   277          require(
   278              provenWithdrawal.timestamp == 0 || oldGame.status() == GameStatus.CHALLENGER_WINS
   279                  || disputeGameBlacklist[oldGame] || oldGame.gameType().raw() != respectedGameType.raw(),
   280              "OptimismPortal: withdrawal hash has already been proven, and the old dispute game is not invalid"
   281          );
   282  
   283          // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract.
   284          // Refer to the Solidity documentation for more information on how storage layouts are
   285          // computed for mappings.
   286          bytes32 storageKey = keccak256(
   287              abi.encode(
   288                  withdrawalHash,
   289                  uint256(0) // The withdrawals mapping is at the first slot in the layout.
   290              )
   291          );
   292  
   293          // Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract
   294          // on L2. If this is true, under the assumption that the SecureMerkleTrie does not have
   295          // bugs, then we know that this withdrawal was actually triggered on L2 and can therefore
   296          // be relayed on L1.
   297          require(
   298              SecureMerkleTrie.verifyInclusionProof(
   299                  abi.encode(storageKey), hex"01", _withdrawalProof, _outputRootProof.messagePasserStorageRoot
   300              ),
   301              "OptimismPortal: invalid withdrawal inclusion proof"
   302          );
   303  
   304          // Designate the withdrawalHash as proven by storing the `disputeGameProxy` & `timestamp` in the
   305          // `provenWithdrawals` mapping. A `withdrawalHash` can only be proven once unless the dispute game it proved
   306          // against resolves against the favor of the root claim.
   307          provenWithdrawals[withdrawalHash] =
   308              ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) });
   309  
   310          // Emit a `WithdrawalProven` event.
   311          emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target);
   312      }
   313  
   314      /// @notice Finalizes a withdrawal transaction.
   315      /// @param _tx Withdrawal transaction to finalize.
   316      function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external whenNotPaused {
   317          // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other
   318          // than the default value when a withdrawal transaction is being finalized. This check is
   319          // a defacto reentrancy guard.
   320          require(
   321              l2Sender == Constants.DEFAULT_L2_SENDER, "OptimismPortal: can only trigger one withdrawal per transaction"
   322          );
   323  
   324          // Compute the withdrawal hash.
   325          bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);
   326  
   327          // Check that the withdrawal can be finalized.
   328          checkWithdrawal(withdrawalHash);
   329  
   330          // Mark the withdrawal as finalized so it can't be replayed.
   331          finalizedWithdrawals[withdrawalHash] = true;
   332  
   333          // Set the l2Sender so contracts know who triggered this withdrawal on L2.
   334          l2Sender = _tx.sender;
   335  
   336          // Trigger the call to the target contract. We use a custom low level method
   337          // SafeCall.callWithMinGas to ensure two key properties
   338          //   1. Target contracts cannot force this call to run out of gas by returning a very large
   339          //      amount of data (and this is OK because we don't care about the returndata here).
   340          //   2. The amount of gas provided to the execution context of the target is at least the
   341          //      gas limit specified by the user. If there is not enough gas in the current context
   342          //      to accomplish this, `callWithMinGas` will revert.
   343          bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data);
   344  
   345          // Reset the l2Sender back to the default value.
   346          l2Sender = Constants.DEFAULT_L2_SENDER;
   347  
   348          // All withdrawals are immediately finalized. Replayability can
   349          // be achieved through contracts built on top of this contract
   350          emit WithdrawalFinalized(withdrawalHash, success);
   351  
   352          // Reverting here is useful for determining the exact gas cost to successfully execute the
   353          // sub call to the target contract if the minimum gas limit specified by the user would not
   354          // be sufficient to execute the sub call.
   355          if (!success && tx.origin == Constants.ESTIMATION_ADDRESS) {
   356              revert("OptimismPortal: withdrawal failed");
   357          }
   358      }
   359  
   360      /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in
   361      ///         deriving deposit transactions. Note that if a deposit is made by a contract, its
   362      ///         address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider
   363      ///         using the CrossDomainMessenger contracts for a simpler developer experience.
   364      /// @param _to         Target address on L2.
   365      /// @param _value      ETH value to send to the recipient.
   366      /// @param _gasLimit   Amount of L2 gas to purchase by burning gas on L1.
   367      /// @param _isCreation Whether or not the transaction is a contract creation.
   368      /// @param _data       Data to trigger the recipient with.
   369      function depositTransaction(
   370          address _to,
   371          uint256 _value,
   372          uint64 _gasLimit,
   373          bool _isCreation,
   374          bytes memory _data
   375      )
   376          public
   377          payable
   378          metered(_gasLimit)
   379      {
   380          // Just to be safe, make sure that people specify address(0) as the target when doing
   381          // contract creations.
   382          if (_isCreation) {
   383              require(_to == address(0), "OptimismPortal: must send to address(0) when creating a contract");
   384          }
   385  
   386          // Prevent depositing transactions that have too small of a gas limit. Users should pay
   387          // more for more resource usage.
   388          require(_gasLimit >= minimumGasLimit(uint64(_data.length)), "OptimismPortal: gas limit too small");
   389  
   390          // Prevent the creation of deposit transactions that have too much calldata. This gives an
   391          // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure
   392          // that the transaction can fit into the p2p network policy of 128kb even though deposit
   393          // transactions are not gossipped over the p2p network.
   394          require(_data.length <= 120_000, "OptimismPortal: data too large");
   395  
   396          // Transform the from-address to its alias if the caller is a contract.
   397          address from = msg.sender;
   398          if (msg.sender != tx.origin) {
   399              from = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
   400          }
   401  
   402          // Compute the opaque data that will be emitted as part of the TransactionDeposited event.
   403          // We use opaque data so that we can update the TransactionDeposited event in the future
   404          // without breaking the current interface.
   405          bytes memory opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data);
   406  
   407          // Emit a TransactionDeposited event so that the rollup node can derive a deposit
   408          // transaction for this deposit.
   409          emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData);
   410      }
   411  
   412      /// @notice Blacklists a dispute game. Should only be used in the event that a dispute game resolves incorrectly.
   413      /// @param _disputeGame Dispute game to blacklist.
   414      function blacklistDisputeGame(IDisputeGame _disputeGame) external {
   415          require(msg.sender == guardian(), "OptimismPortal: only the guardian can blacklist dispute games");
   416          disputeGameBlacklist[_disputeGame] = true;
   417      }
   418  
   419      /// @notice Sets the respected game type. Changing this value can alter the security properties of the system,
   420      ///         depending on the new game's behavior.
   421      /// @param _gameType The game type to consult for output proposals.
   422      function setRespectedGameType(GameType _gameType) external {
   423          require(msg.sender == guardian(), "OptimismPortal: only the guardian can set the respected game type");
   424          respectedGameType = _gameType;
   425          respectedGameTypeUpdatedAt = uint64(block.timestamp);
   426      }
   427  
   428      /// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be
   429      ///         finalized, and otherwise has no side-effects.
   430      /// @param _withdrawalHash Hash of the withdrawal to check.
   431      function checkWithdrawal(bytes32 _withdrawalHash) public view {
   432          ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash];
   433          IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy;
   434  
   435          // The dispute game must not be blacklisted.
   436          require(!disputeGameBlacklist[disputeGameProxy], "OptimismPortal: dispute game has been blacklisted");
   437  
   438          // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has
   439          // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have
   440          // a timestamp of zero.
   441          require(provenWithdrawal.timestamp != 0, "OptimismPortal: withdrawal has not been proven yet");
   442  
   443          uint64 createdAt = disputeGameProxy.createdAt().raw();
   444  
   445          // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than
   446          // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of
   447          // safety against weird bugs in the proving step.
   448          require(
   449              provenWithdrawal.timestamp > createdAt,
   450              "OptimismPortal: withdrawal timestamp less than dispute game creation timestamp"
   451          );
   452  
   453          // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing.
   454          require(
   455              block.timestamp - provenWithdrawal.timestamp > PROOF_MATURITY_DELAY_SECONDS,
   456              "OptimismPortal: proven withdrawal has not matured yet"
   457          );
   458  
   459          // A proven withdrawal must wait until the dispute game it was proven against has been
   460          // resolved in favor of the root claim (the output proposal). This is to prevent users
   461          // from finalizing withdrawals proven against non-finalized output roots.
   462          require(
   463              disputeGameProxy.status() == GameStatus.DEFENDER_WINS,
   464              "OptimismPortal: output proposal has not been finalized yet"
   465          );
   466  
   467          // The game type of the dispute game must be the respected game type. This was also checked in
   468          // `proveWithdrawalTransaction`, but we check it again in case the respected game type has changed since
   469          // the withdrawal was proven.
   470          require(disputeGameProxy.gameType().raw() == respectedGameType.raw(), "OptimismPortal: invalid game type");
   471  
   472          // The game must have been created after `respectedGameTypeUpdatedAt`. This is to prevent users from creating
   473          // invalid disputes against a deployed game type while the off-chain challenge agents are not watching.
   474          require(
   475              createdAt >= respectedGameTypeUpdatedAt,
   476              "OptimismPortal: dispute game created before respected game type was updated"
   477          );
   478  
   479          // Before a withdrawal can be finalized, the dispute game it was proven against must have been
   480          // resolved for at least `DISPUTE_GAME_FINALITY_DELAY_SECONDS`. This is to allow for manual
   481          // intervention in the event that a dispute game is resolved incorrectly.
   482          require(
   483              block.timestamp - disputeGameProxy.resolvedAt().raw() > DISPUTE_GAME_FINALITY_DELAY_SECONDS,
   484              "OptimismPortal: output proposal in air-gap"
   485          );
   486  
   487          // Check that this withdrawal has not already been finalized, this is replay protection.
   488          require(!finalizedWithdrawals[_withdrawalHash], "OptimismPortal: withdrawal has already been finalized");
   489      }
   490  }