Skip to main content
When a deposit transaction fails on L2 — usually because it ran out of gas or the L2 state didn’t allow it to succeed — the message isn’t lost. L2CrossDomainMessenger records it as a failed message, and you can replay it later, optionally with more gas. In this tutorial, you’ll make a deposit fail on purpose against a test contract, then replay it successfully, so you can see the full failure-and-replay cycle end to end. For the concepts behind deposits and why replays are possible, see Deposit flow.

Before you begin

  • Foundry installed (this tutorial uses cast).
  • A test account private key, funded with a small amount of test ETH on both Ethereum Sepolia (L1) and OP Sepolia (L2).
  • An L1 (Ethereum Sepolia) RPC URL — e.g. a free Infura key or another provider.
L1 vs L2 network clarificationThis tutorial involves two different networks:
  • L1: Ethereum Sepolia testnet (https://sepolia.infura.io/v3/YOUR_KEY)
  • L2: OP Sepolia testnet (https://sepolia.optimism.io)
You’ll send transactions on L1 that trigger actions on L2. Make sure you’re using the correct RPC URLs for each step.

Trigger and replay a failed deposit

To see how replays work, you can use this contract on OP Sepolia.
  1. Call stopChanges, using this Foundry command:
  2. Verify that getStatus() returns false, meaning changes are not allowed, and see the value of greet() using Foundry. Note that Foundry returns false as zero.
  3. Get the calldata. You can use this Foundry command:
    Or just use this value:
  4. Send a greeting change as a deposit from L1 (Ethereum Sepolia) to L2 (OP Sepolia). Use these commands:
    The transaction will be successful on L1 (Ethereum Sepolia), but then emit a fail event on L2 (OP Sepolia).
  5. The next step is to find the hash of the failed relay. There are several ways to do this: Method A: Using Etherscan Internal Transactions Look in the internal transactions of the destination contract, and select the latest one that appears as a failure. It should be a call to L2CrossDomainMessenger at address 0x420...007. Method B: Using Contract Events (if internal transactions aren’t visible) If you can’t see internal transactions on Etherscan, check the L2CrossDomainMessenger contract events and look for FailedRelayedMessage events with your contract address. Method C: Using cast to query failed messages
    If the latest internal transaction is a success, it probably means your transaction hasn’t relayed yet. Wait until it is, that may take a few minutes.
  6. Get the transaction information using Foundry.
    Wait for the failed relay transactionMake sure you wait for the deposit to be processed on L2 and fail before proceeding. This can take 2-5 minutes. You should see a failed transaction in one of the methods from step 5.
  7. Call startChanges() to allow changes using this Foundry command:
    Don’t do this prematurelyIf you call startChanges() too early, it will happen when the message is relayed to L2, and then the initial deposit will be successful and there will be no need to replay it.
  8. Verify that getStatus() returns true, meaning changes are not allowed, and see the value of greet(). Foundry returns true as one.
  9. Now send the replay transaction.
    Why do we need to specify the gas limit?The gas estimation mechanism tries to find the minimum gas limit at which the transaction would be successful. However, L2CrossDomainMessenger does not revert when a replay fails due to low gas limit, it just emits a failure message. The gas estimation mechanism considers that a success.To get a gas estimate, you can use this command:
    That address is a special case in which the contract does revert.
  10. Verify the greeting has changed:

Debugging

To debug deposit transactions, you can ask the L2 cross domain messenger for the state of the transaction.
  1. Look on Etherscan to see the FailedRelayedMessage event. Set MSG_HASH to that value.
  2. To check if the message is listed as failed, run this:
    To check if it is listed as successful, run this:

Next steps