Deposit Flow
# Introduction
In Optimism terminology, "deposit transaction" refers to any L2 transaction that is triggered by a transaction or event on L1. A deposit transaction may or may not have assets (ETH, tokens, etc.) attached to it.
The process is somewhat similar to the way most networking stacks work (opens new window). Information is encapsulated in lower layer packets on the sending side, and then retrieved and used by those layers on the receiving side while going up the stack to the receiving application.
# L1 Processing
An L1 entity, either a smart contract or an externally owned account (EOA), sends a deposit transaction to
L1CrossDomainMessenger
(opens new window), usingsendMessage
(opens new window). This function accepts three parameters:_target
, target address on L2._message
, the L2 transaction's calldata, formatted as per the ABI (opens new window) of the target account._minGasLimit
, the minimum gas limit allowed for the transaction on L2. Note that this is a minimum and the actual amount provided on L2 may be higher (but never lower) than the specified gas limit. Note that the actual amount provided on L2 will be higher, because the portal contract on L2 needs to do some processing before submitting the call to_target
.
You can see code that implements this call in the tutorial (opens new window).
The L1 cross domain messenger calls its own
_send
function (opens new window). It uses these parameters:_to
, the destination address, is the messenger on the other side. In the case of deposits, this is always0x4200000000000000000000000000000000000007
(opens new window)._gasLimit
, the gas limit. This value is calculated using thebaseGas
function (opens new window)._value
, the ETH that is sent with the message. This amount is taken from the transaction value._data
, the calldata for the call on L2 that is needed to relay the message. This is an ABI encoded (opens new window) call torelayMessage
(opens new window).
_sendMessage
(opens new window) calls the portal'sdepositTransaction
function (opens new window).Note that other contracts can also call
depositTransaction
(opens new window) directly. However, doing so bypasses certain safeguards, so in most cases it's a bad idea.The
depositTransaction
function (opens new window) runs a few sanity checks, and then emits aTransactionDeposited
(opens new window) event.
# L2 Processing
The
op-node
component looks forTransactionDeposited
events on L1 (opens new window). If it sees any such events, it parses (opens new window) them.Next,
op-node
converts (opens new window) thoseTransactionDeposited
events into deposit transactions (opens new window).In most cases user deposit transactions call the
relayMessage
(opens new window) function ofL2CrossDomainMessenger
(opens new window).relayMessage
runs a few sanity checks and then, if everything is good, calls the real target contract with the relayed calldata (opens new window).
# Denial of service (DoS) prevention
As with all other L1 transactions, the L1 costs of a deposit are borne by the transaction's originator. However, the L2 processing of the transaction is performed by the Optimism nodes. If there were no cost attached, an attacker could be able to submit a transaction that had high costs of run on L2, and that way perform a denial of service attack.
To avoid this DoS vector, depositTransaction
(opens new window), and the functions that call it, require a gas limit parameter.
This gas limit is encoded into the []TransactionDeposited
event (opens new window), and used as the gas limit for the user deposit transaction on L2.
This L2 gas is paid for by burning L1 gas here (opens new window).
# Replaying messages
Deposits transactions can fail due to several reasons:
- Not enough gas provided.
- The state on L2 does not allow the transaction to be successful.
It is possible to replay a failed deposit, possibly with more gas,
# Replays in action
To see how replays work, you can use this contract on OP Goerli (opens new window).
Call
stopChanges
, using this Foundry command:PRIV_KEY=<your private key here> export ETH_RPC_URL=<url to OP Goerli> GREETER=0x26A145eccDf258688C763726a8Ab2aced898ADe1 cast send --private-key $PRIV_KEY $GREETER "stopChanges()"
1
2
3
4Verify that
getStatus()
returns false, meaning changes are not allowed, and see the value ofgreet()
using Foundry. Note that Foundry returns false as zero.cast call $GREETER "greet()" | cast --to-ascii ; cast call $GREETER "getStatus()"
1Get the calldata. You can use this Foundry command:
cast calldata "setGreeting(string)" "testing"
1Or just use this value:
0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000774657374696e6700000000000000000000000000000000000000000000000000
1Send a greeting change as a deposit. Use these commands:
L1_RPC=<URL to Goerli> L1XDM_ADDRESS=0x5086d1eef304eb5284a0f6720f79403b4e9be294 FUNC="sendMessage(address,bytes,uint32)" CALLDATA=`cast calldata "setGreeting(string)" "testing"` cast send --rpc-url $L1_RPC --private-key $PRIV_KEY $L1XDM_ADDRESS $FUNC $GREETER $CALLDATA 10000000
1
2
3
4
5The transaction will be successful on L1, but then emit a fail event on L2.
The next step is to find the hash of the failed relay. The easiest way to do this is to look in the internal transactions of the destination contract (opens new window), and select the latest one that appears as a failure. It should be a call to L2CrossDomainMessenger at address
0x420...007
. This is the call you need to replay.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.
Get the transaction information using Foundry.
TX_HASH=<transaction hash from Etherscan> L2XDM_ADDRESS=0x4200000000000000000000000000000000000007 REPLAY_DATA=`cast tx $TX_HASH input`
1
2
3Call
startChanges()
to allow changes using this Foundry command:cast send --private-key $PRIV_KEY $GREETER "startChanges()"
1Don't do this prematurely
If you call
startChanges()
too early, it will happen from the message is relayed to L2, and then the initial deposit will be successful and there will be no need to replay it.Verify that
getStatus()
returns true, meaning changes are not allowed, and see the value ofgreet()
. Foundry returns true as one.cast call $GREETER "greet()" | cast --to-ascii ; cast call $GREETER "getStatus()"
1Now send the replay transaction.
cast send --private-key $PRIV_KEY --gas-limit 10000000 $L2XDM_ADDRESS $REPLAY_DATA
1Why 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:
cast estimate --from 0x0000000000000000000000000000000000000001 $L2XDM_ADDRESS $REPLAY_DATA
1That address is a special case in which the contract does revert.
Verify the greeting has changed:
cast call $GREETER "greet()" | cast --to-ascii ; cast call $GREETER "getStatus()"
1
Debugging
To debug deposit transactions you can ask the L2 cross domain messenger for the state of the transaction.
Look on Etherscan to see the
FailedRelayedMessage
event. SetMSG_HASH
to that value.To check if the message is listed as failed, run this:
cast call $L2XDM_ADDRESS "failedMessages(bytes32)" $MSG_HASH
1To check if it is listed as successful, run this:
cast call $L2XDM_ADDRESS "successfulMessages(bytes32)" $MSG_HASH
1