# Building Bridges

## Overview

For the high-level flow of how Facet derives state and how canonical roots are exposed on L1 for bridges, see the [Architecture Overview](https://docs.facet.org/introduction/architecture-overview).

Facet has no canonical bridge, but anyone can build and deploy a bridge. This guide covers the complete process from understanding bridge mechanics to deploying production-ready contracts.

## How Bridging Works

### Architecture Overview

Bridges on Facet consist of two main components:

1. **L1 Contract**: Handles deposits and withdrawals on Ethereum
2. **L2 Contract**: Manages the L2 token representation

### Depositing Assets (L1 → Facet)

{% stepper %}
{% step %}
**Submit Deposit**

User deposits assets (e.g., USDC, WETH) into an L1 bridge contract.
{% endstep %}

{% step %}
**Bridge Emits Event**

The bridge contract emits an event with the Facet event signature:

```
0x00000000000000000000000000000000000000000000000000000000000face7
```

This event contains the deposit details in RLP-encoded format.
{% endstep %}

{% step %}
**Facet Nodes Process**

Facet nodes monitor L1 for these events and convert them into L2 transactions from the L1 bridge contract to the L2 bridge contract.
{% endstep %}

{% step %}
**Assets Available on Facet**

The L2 bridge contract credits the user's L2 account with the bridged assets when it receives the L2 transaction.
{% endstep %}
{% endstepper %}

### Withdrawing Assets (Facet → L1)

Withdrawals use Facet's ZK Fault Proof system:

{% stepper %}
{% step %}
**Initialize Withdrawal**

User burns L2 assets through the bridge's L2 contract, which calls `L2ToL1MessagePasser` predeploy.
{% endstep %}

{% step %}
**Wait for State Root**

The withdrawal is included in a Facet state root. Proposers post roots to `Rollup.sol`.
{% endstep %}

{% step %}
**Prove Withdrawal**

Once the root is accepted, user submits a proof that their withdrawal exists.
{% endstep %}

{% step %}
**Claim Assets**

After any bridge-specific delays, user claims assets from the L1 bridge contract.
{% endstep %}
{% endstepper %}

## Implementation Guide

This section walks through the reference ETH bridge implementation in zk-fault-proofs. Source:

* L1: <https://github.com/0xFacet/zk-fault-proofs/blob/facet/contracts/src/L1Bridge.sol>
* L2: <https://github.com/0xFacet/zk-fault-proofs/blob/facet/contracts/src/L2Bridge.sol>

### 1) L1Bridge: Deposits, Proofs, Finalization

Key capabilities:

* Deposits ETH to L2 with replay support if L2 blocks are full
* Optional training wheels (pause, withdrawal delay, root blacklist)
* Fork handling via `setRollup` (owner-controlled or renounced)

Core deposit data and send path:

```solidity
// Deposit parameters recorded for replay
struct DepositTransaction {
    uint256 nonce;   // unique deposit id
    address to;      // L2 recipient
    uint256 amount;  // ETH amount in wei
}

// Store deposit hash and send to L2 via 0xface7
function initiateDeposit(address to) public payable returns (uint256 nonce) {
    nonce = ++depositNonce;
    DepositTransaction memory d = DepositTransaction({ nonce: nonce, to: to, amount: msg.value });
    depositHashes[nonce] = _hashDeposit(d);

    bytes memory data = abi.encodeWithSelector(L2Bridge.finalizeDeposit.selector, d);
    LibFacet.sendFacetTransaction({ to: l2Bridge, gasLimit: 500_000, data: data });
}

// Retry with identical parameters if the previous attempt failed due to full blocks
function replayDeposit(DepositTransaction calldata d) external {
    require(depositHashes[d.nonce] == _hashDeposit(d), "param mismatch");
    bytes memory data = abi.encodeWithSelector(L2Bridge.finalizeDeposit.selector, d);
    LibFacet.sendFacetTransaction({ to: l2Bridge, gasLimit: 500_000, data: data });
}
```

EOA convenience path:

```solidity
// EOA-only direct deposit to self (supports EIP-7702 delegated EOAs)
receive() external payable {
    if (!EOA.isSenderEOA()) revert OnlyCanDepositWithoutTo();
    initiateDeposit(msg.sender);
}
```

Training wheels and fork handling:

```solidity
// Update Rollup contract used for proofs (owner-controlled)
function setRollup(address _rollup) external onlyOwner { /* ... */ }

// Pause/unpause and safety delay
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
function setWithdrawalDelay(uint256 secs) external onlyOwner { /* ... */ }

// Blacklist a specific root (by hash) if needed
function setRootBlacklisted(bytes32 root, bool blacklisted) external onlyOwner { /* ... */ }
```

Withdrawal verification and finalization:

```solidity
// Prove a withdrawal exists in canonical L2 state
function proveWithdrawal(
    address to,
    uint256 amount,
    uint256 nonce,
    uint256 proposalId,
    Types.OutputRootProof calldata rootProof,
    bytes[] calldata withdrawalProof
) external {
    bytes32 withdrawalHash = _hashWithdrawal(to, amount, nonce);
    require(!finalized[withdrawalHash], "already finalized");

    // Ensure the proposal is canonical
    if (!rollup.proposalIsCanonical(proposalId)) revert ProposalNotCanonical();
    Rollup.Proposal memory prop = rollup.getProposal(proposalId);

    // Optional safety check
    if (rootBlacklisted[prop.rootClaim]) revert RootBlacklisted();

    // Root and storage proof verification
    if (prop.rootClaim != Hashing.hashOutputRootProof(rootProof)) revert InvalidOutputRoot();
    bytes32 storageKey = keccak256(abi.encode(withdrawalHash, uint256(0))); // slot 0
    bool ok = SecureMerkleTrie.verifyInclusionProof({
        _key: abi.encode(storageKey),
        _value: hex"01",
        _proof: withdrawalProof,
        _root: rootProof.messagePasserStorageRoot
    });
    if (!ok) revert InvalidWithdrawalProof();

    proven[withdrawalHash][rollup] = ProvenWithdrawal({ proposalId: uint32(proposalId), provenAt: uint32(block.timestamp) });
}

// Finalize after delay and safety checks
function finalizeWithdrawal(address to, uint256 amount, uint256 nonce) external {
    bytes32 withdrawalHash = _hashWithdrawal(to, amount, nonce);
    ProvenWithdrawal memory info = proven[withdrawalHash][rollup];
    if (info.provenAt == 0) revert WithdrawalNotProven();
    if (block.timestamp <= info.provenAt + withdrawalDelay) revert WithdrawalDelayNotMet();
    Rollup.Proposal memory prop = rollup.getProposal(info.proposalId);
    if (rootBlacklisted[prop.rootClaim]) revert RootBlacklisted();
    finalized[withdrawalHash] = true;
    to.forceSafeTransferETH(amount, SafeTransferLib.GAS_STIPEND_NO_STORAGE_WRITES);
}
```

Trust model note: With ownership renounced, the bridge becomes human-free but permanently locked to one Rollup contract (fork). With ownership active, the operator can switch Rollup to follow new rules—introducing operator trust but preserving flexibility.

### 2) L2Bridge: Finalize Deposits, Initiate Withdrawals

Core responsibilities:

* Finalize deposits from L1 with nonce-based replay protection
* Initiate withdrawals to L1 by burning and calling the message passer

```solidity
// Only the aliased L1 bridge can finalize deposits
modifier onlyL1Bridge() {
    if (msg.sender != AddressAliasHelper.applyL1ToL2Alias(l1Bridge)) revert UnauthorizedBridge();
    _;
}

// Finalize deposit exactly once per nonce
function finalizeDeposit(L1Bridge.DepositTransaction calldata d) external onlyL1Bridge {
    if (finalizedDeposits[d.nonce]) revert DepositAlreadyFinalized();
    finalizedDeposits[d.nonce] = true;
    _mint(d.to, d.amount); // wrap ETH on L2
}

// Burn on L2 and send withdrawal message to L1
function initiateWithdrawal(address to, uint256 amount) external {
    if (amount == 0) revert InvalidWithdrawalAmount();
    _burn(msg.sender, amount);
    bytes memory data = abi.encode(to, amount);
    MESSAGE_PASSER.initiateWithdrawal(l1Bridge, 0, data);
}
```

### 3) Putting It Together

End-to-end flow:

* Deposit (L1 → L2): Users send ETH to `L1Bridge.initiateDeposit`. The bridge records the deposit (nonce + hash) and sends a Facet transaction to `L2Bridge.finalizeDeposit`. If an L2 block is full, `replayDeposit` can resend the same deposit (same nonce/params).
* Withdraw (L2 → L1): Users call `L2Bridge.initiateWithdrawal` to burn wrapped ETH and create a message in `L2ToL1MessagePasser`. On L1, users prove the withdrawal via `L1Bridge.proveWithdrawal` using the canonical root in `Rollup.sol` and finalize after any delay.

Security & safety gears:

* Replay-safe deposits (nonce + hash) and single-use finalize on L2
* Optional pause, withdrawal delay, and root blacklist
* Fork handling via `setRollup` (trust vs flexibility)
