Assessment reports>Facet Bridge>Design>L1 → L2 flow

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.

Zellic © 2025Back to top ↑