L1 → L2 flow
This section summarizes the logic flow of funds bridged from L1 (Ethereum) to Facet (L2).
Note: The description ignores events emitted by the contracts that are not strictly needed for the bridge operation.
L1 side
To initiate a deposit, a user must first grant approval for the token to be bridged to the bridge contract. The user then calls the bridge depositERC20
or depositERC20To
functions:
function depositERC20(
address _l1Token,
address _l2Token,
uint256 _amount,
uint32 _minGasLimit,
bytes calldata _extraData
) external virtual onlyEOA { /* ... */ }
function depositERC20To(
address _l1Token,
address _l2Token,
address _to,
uint256 _amount,
uint32 _minGasLimit,
bytes calldata _extraData
) external virtual { /* ... */ }
The two functions invoke the same underlying function _initiateBridgeERC20
; the only difference between the two is depositERC20
implicitly sets the address of the receiver of the funds to the address of the sender.
The _initiateBridgeERC20
function performs the two critical operations needed to initiate the L1 → L2 bridging: 1) taking funds from the sender address and 2) emitting an event that triggers a transaction on the L2 invoking the destination bridge.
How funds are taken from the sender address varies on the nature of the source token. If the source token is a regular ERC-20 that originates on the L1, the amount to be bridged is taken from the sender using transferFrom
. The source token could also be a representation of an ERC-20 that originated and was first bridged from the L2 — in other words, the ERC-20 could be a token minted by the bridge when an L2-native asset has bridged to the L1. The bridge assumes this is the case if the token contract declares support for the IOptimismMintableERC20
or ILegacyMintableERC20
interfaces. In this case, the bridge burns the tokens directly from the user balance.
The function then computes a unique deposit ID. This deposit ID is associated in a map to the hash of a struct describing the deposit (the addresses of the source and destination tokens, the addresses of the source and recipient, the asset amount, and an arbitrary bytestring that can be used freely, e.g., to facilitate off-chain processing). Therefore, the bridge maintains a mapping with one record for each successful L1 deposit.
Finally, _initiateBridgeERC20
invokes replayERC20Deposit
, which uses LibFacet.sendFacetTransaction
to invoke the L2 bridge contract and finalize the bridging operation:
LibFacet.sendFacetTransaction({
gasLimit: 500_000,
to: address(otherBridge),
data: abi.encodeWithSelector(
this.finalizeBridgeERC20Replayable.selector,
_depositId,
_payload
)
});
L2 side
The transaction triggered by replayERC20Deposit
generates a call to the finalizeBridgeERC20Replayable
function of the L2 bridge contract, which receives the deposit ID identifying the operation and the payload containing the operation parameters (tokens, source and recipient addresses, amount, and extra data).
The finalizeBridgeERC20Replayable
function ensures that the call originates from the L1 bridge contract; this establishes the trustworthiness of the ID and payload.
It then ensures that the deposit ID is not already recorded as processed and marks it as such, preventing attempts to repeat the same deposit multiple times.
Finally, finalizeBridgeERC20
is called; this function is responsible for minting or releasing the locked destination assets held in escrow to the recipient.
Under normal operation, if the source asset was an L1-native ERC-20, the destination asset is set to the address of an OptimismMintableERC20 contract owned by the bridge, which represents the asset being bridged from the L1. After ensuring that the L2 destination asset matches the L1 source asset, the bridge invokes the L2 asset mint
function to mint tokens that represent the bridged L1 assets; the minted tokens are credited to the recipient chosen by the L1 sender.
Alternatively, the source asset could be an OptimismMintableERC20 contract representing an L2-native ERC-20 that was bridged to the L1. In this case, the destination asset would be set to the address of the L2-native ERC-20 asset, which the bridge is holding in escrow. The amount being bridged is released to the recipient via safeTransfer
.
Sequence diagram
The following sequence diagram shows the logic flow of an L1 to L2 bridging operation.
sequenceDiagram
actor U as User
participant L1B as L1StandardBridge
participant L2B as L2StandardBridge
U ->> L1B: depositERC20(l1Token, l2Token, amount, ...)
L1B ->> L1B: _initiateBridgeERC20
alt Source token is L1 native ERC20
note over L1B: safeTransferFrom amount to bridge from sender<br>Increase variable keeping tack of deposit flows
else Source token was bridged from L2
note over L1B: Burn amount of L1 token to bridge from sender balance
end
note over L1B: Generate unique deposit ID
note over L1B: Compute deposit payload (L1/L2 tokens, sender/recipient addr. and amount)
note over L1B: Associate deposit payload hash with deposit ID
L1B ->> L1B: replayERC20Deposit(depositID, payload)
L1B ->> L1B: LibFacet.sendFacetTransaction(<call finalizeBridgeERC20Replayable on L2 bridge>)
note over L1B: Event emitted triggers a transaction on Facet L2
L1B -->> L2B: finalizeBridgeERC20Replayable(depositId, payload)
note over L2B: Check sender is L1 bridge<br>Check deposit not already processed<br>
L2B ->> L2B: finalizeBridgeERC20(...)
alt Destination token represents native L1 ERC20
note over L2B: Ensure source and destination tokens match<br>Mint tokens to recipient
else Destination token is native L2 ERC20
note over L2B: Decrease variable keeping track of deposits<br>safeTransfer bridged amount to recipient
end
Replaying failed L2 transactions
Unlike most L2s, Facet does not have a sequencer queue; all Facet transactions originating in a given Ethereum block are processed in a Facet L2 block.
However, the Facet L2 blocks have a total gas limit. At the time of this writing, the exact limit was not finalized, but the Facet development team indicated the limit would be very high compared to ordinary L1s. Nevertheless, the limit could potentially be reached, in which case L2 transactions would be rejected without being rescheduled for execution in a subsequent block.
Facet Bridge accounts for this eventuality by allowing anyone to repeat the triggering of the L2 flow of any L1 deposit. Anyone can invoke the replayERC20Deposit
on the L1 bridge using the deposit ID and payload of a successful deposit, which will generate an L2 transaction calling finalizeBridgeERC20Replayable
to finalize that deposit.
This mechanism is secure because the deposit ID and payload are validated against the recorded successful deposits by the L1 bridge and because the L2 bridge ensures that each deposit ID is only processed once.
Native ETH bridging
Facet Bridge exclusively supports bridging ERC-20 assets; native ETH can be bridged by wrapping it using a WETH contract and following the regular ERC-20 flow. Facet Bridge provides a convenience function, depositWethTo
, which performs the wrapping on behalf of the user.