Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
071da34
WIP
qbzzt Apr 12, 2025
f9c3040
WIP
qbzzt Apr 12, 2025
75b3ce2
Working 1st draft
qbzzt Apr 14, 2025
b3535ad
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Apr 14, 2025
83c8a2a
Full version
qbzzt Apr 17, 2025
29bf26a
@zainbacchus comments
qbzzt Apr 17, 2025
bb9a679
Merge branch 'main' into 250412-custom-bridge
qbzzt Apr 17, 2025
8e8648e
Update custom-bridge.mdx
qbzzt Apr 18, 2025
200de29
Merge branch 'main' into 250412-custom-bridge
qbzzt Apr 18, 2025
28b4496
Maybe now it'll build
qbzzt Apr 21, 2025
fec0467
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Apr 21, 2025
babb2b6
Maybe now?
qbzzt Apr 21, 2025
98340fe
Fix nav
bradleycamacho Apr 21, 2025
8d6f41f
Merge branch '250412-custom-bridge' of https://github.com/qbzzt/optim…
bradleycamacho Apr 21, 2025
2fcc9d7
Uppercase Supersim
qbzzt Apr 21, 2025
4fd1d18
Apply suggestions from code review
qbzzt Apr 21, 2025
bb44dfd
Fixed hashed and code lines
qbzzt Apr 21, 2025
63cb35f
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt Apr 21, 2025
6e20053
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt Apr 21, 2025
e578ccd
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt Apr 21, 2025
b9b74d9
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt Apr 21, 2025
2159558
lint
qbzzt Apr 21, 2025
3efad2f
lint
qbzzt Apr 21, 2025
f33ceff
lint, yet another issue
qbzzt Apr 21, 2025
e633a05
Merge branch 'main' into 250412-custom-bridge
krofax May 15, 2025
a39d55f
Merge branch 'main' into 250412-custom-bridge
qbzzt May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pages/interop/tutorials/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"relay-messages-viem": "Relaying interop messages using `viem`",
"contract-calls": "Making crosschain contract calls (ping pong)",
"event-reads": "Making crosschain event reads (tic-tac-toe)",
"event-contests": "Deploying crosschain event composability (contests)"
"event-contests": "Deploying crosschain event composability (contests)",
"upgrade-to-superchain-erc20": "Upgrade to SuperchainERC20"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"custom-bridge": "Building a custom bridge"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
---
title: Building a custom bridge
lang: en-US
description: Tutorial on how to create a custom interoperability bridge. The example is a bridge when the addresses of the ERC20 contracts are not the same.
topic: Interoperability
personas: [Developer]
categories: [Tutorial, Interop]
content_type: article
---

import { Steps, Callout, Tabs } from 'nextra/components'

# Building a custom bridge

## Overview

Sometimes the address of an ERC20 contract is not available on a different chain.
This means that the [SuperchainTokenBridge](/interop/superchain-erc20) is not an option.
However, if the original ERC20 contract is behind a proxy (so we can add [ERC7802](https://eips.ethereum.org/EIPS/eip-7802) support), we can still use interop by writing our own bridge.

<details>
<summary>About this tutorial</summary>

**What you'll learn**

* How to use [interop message passing](/interop/tutorials/message-passing) to create a custom bridge.

**Prerequisite knowledge**

* How to [deploy SuperchainERC20 tokens with custom code](/interop/tutorials/custom-superchain-erc20).
* How to [transfer interop messages](/interop/tutorials/message-passing).
</details>

<Callout type="warning">
The code on the documentation site is sample code, *not* production code.
This means that we ran it, and it works as advertised.
However, it did not pass through the rigorous audit process that most Optimism code undergoes.
You're welcome to use it, but if you need it for production purposes you should get it audited first.
</Callout>

{/*

I put this warning here, when we don't have it on most pages, because this tutorial
has code that is a lot more likely to be used in production. It doesn't just
show what is possible, it does the exact job needed.

*/}

## How beacon proxies work

```mermaid
sequenceDiagram
Actor User
User->>BeaconProxy: transfer(<address>, <amount>)
BeaconProxy->>UpgradeableBeacon: What is the implementation address?
UpgradeableBeacon->>BeaconProxy: It is 0xBAD0...60A7
BeaconProxy->>0xBAD0...60A7: transfer(<address>, <amount>)
```

A [beacon proxy](https://docs.openzeppelin.com/contracts/3.x/api/proxy#BeaconProxy) uses two contracts.
The [`UpgradeableBeacon`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/UpgradeableBeacon.sol) contract holds the address of the implementation contract.
The [`BeaconProxy`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/BeaconProxy.sol) contract is the one called for the functionality, the one that holds the storage.
When a user (or another contract) calls `BeaconProxy`, it asks `UpgradeableBeacon` for the implementation address and then uses [`delegatecall`](https://www.evm.codes/?fork=cancun#f4) to call that contract.

```mermaid
sequenceDiagram
Actor User
Actor Owner
Participant BeaconProxy
Participant 0x600D...60A7
Owner->>UpgradeableBeacon: Your new implementation address is 0x600D...60A7
User->>BeaconProxy: transfer(<address>, <amount>)
BeaconProxy->>UpgradeableBeacon: What is the implementation address?
UpgradeableBeacon->>BeaconProxy: It is 0x600D...60A7
BeaconProxy->>0x600D...60A7: transfer(<address>, <amount>)
```

To upgrade the contract, an authorized address (typically the `Owner`) calls `UpgradeableBeacon` directly to specify the new implementation contract address.
After that happens, all new calls are sent to the new implementation.

## Instructions

Some steps depend on whether you want to deploy on [Supersim](/interop/tools/supersim) or on the [development network](/interop/tools/devnet).

<Steps>
### Install and run Supersim

If you are going to use Supersim, [follow these instructions](/app-developers/tutorials/supersim/getting-started/installation) to install and run Supersim.

<Callout>
Make sure to run Supersim with autorelay on.

```sh
./supersim --interop.autorelay true
```
</Callout>

### Set up the ERC20 token on chain A

Download and run the setup script.

```sh
curl https://docs.optimism.io/tutorials/setup-for-erc20-upgrade.sh > setup-for-erc20-upgrade.sh
chmod +x setup-for-erc20-upgrade.sh
./setup-for-erc20-upgrade.sh
```

If you want to deploy to the [development networks](/interop/tools/devnet), provide `setup-for-erc20-upgrade.sh` with the private key of an address with ETH on both devnets.

```sh
./setup-for-erc20-upgrade.sh <private key>
```

### Store the addresses

Execute the bottom two lines of the setup script output to store the ERC20 address and the address of the beacon contract.

```sh
BEACON_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
export ERC20_ADDRESS=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
```

### Specify environment variables

Specify these variables, which we use later:

<Tabs items={['Supersim', 'Devnets']}>
<Tabs.Tab>
Set these parameters for Supersim.

```sh
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
USER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
URL_CHAIN_A=http://127.0.0.1:9545
URL_CHAIN_B=http://127.0.0.1:9546
```
</Tabs.Tab>

<Tabs.Tab>
For Devnet, specify in `PRIVATE_KEY` the private key you used for the setup script and then these parameters.

```sh
USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY`
URL_CHAIN_A=https://interop-alpha-0.optimism.io
URL_CHAIN_B=https://interop-alpha-1.optimism.io
```
</Tabs.Tab>
</Tabs>

### Advance the user's nonce on chain B

This solution is necessary when the nonce on chain B is higher than it was on chain A when the proxy for the ERC-20 contract was installed.
To simulate this situation, we send a few meaningless transactions on chain B and then see that the nonce on B is higher than the nonce on A.

```sh
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
echo -n Nonce on chain A:
cast nonce $USER_ADDRESS --rpc-url $URL_CHAIN_A
echo -n Nonce on chain B:
cast nonce $USER_ADDRESS --rpc-url $URL_CHAIN_B
```

### Create a Foundry project

Create a [Foundry](https://book.getfoundry.sh/) project and import the [OpenZeppelin](https://www.openzeppelin.com/solidity-contracts) contracts used for the original ERC20 and proxy deployment.

```sh
mkdir custom-bridge
cd custom-bridge
forge init
forge install OpenZeppelin/openzeppelin-contracts
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
forge install ethereum-optimism/interop-lib
```

### Deploy proxies

We need two contracts on each chain: an ERC20 and a bridge, and to enable future upgrades, we want to install each of those contracts behind a proxy.
You already have one contract—the ERC20 on chain A—but need to create the other three.

There's an interesting [chicken-and-egg](https://en.wikipedia.org/wiki/Chicken_or_the_egg) issue here.
To create a proxy, we need the address of the implementation contract, the one with the actual code.
However, the bridge and ERC20 code needs to have the proxy addresses.
One possible solution is to choose a pre-existing contract, and use that as the implementation contract until we can upgrade.
Every OP Stack chain has [predeploys](https://specs.optimism.io/protocol/predeploys.html) we can use for this purpose.

```sh
DUMMY_ADDRESS=0x4200000000000000000000000000000000000000
UPGRADE_BEACON_CONTRACT=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol:UpgradeableBeacon
PROXY_CONTRACT=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/BeaconProxy.sol:BeaconProxy
BRIDGE_BEACON_ADDRESS_A=`forge create $UPGRADE_BEACON_CONTRACT --broadcast --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY --constructor-args $DUMMY_ADDRESS $USER_ADDRESS | awk '/Deployed to:/ {print $3}'`
BRIDGE_ADDRESS_A=`forge create $PROXY_CONTRACT --broadcast --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY --constructor-args $BRIDGE_BEACON_ADDRESS_A 0x | awk '/Deployed to:/ {print $3}'`
BRIDGE_BEACON_ADDRESS_B=`forge create $UPGRADE_BEACON_CONTRACT --broadcast --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY --constructor-args $DUMMY_ADDRESS $USER_ADDRESS | awk '/Deployed to:/ {print $3}'`
BRIDGE_ADDRESS_B=`forge create $PROXY_CONTRACT --broadcast --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY --constructor-args $BRIDGE_BEACON_ADDRESS_B 0x | awk '/Deployed to:/ {print $3}'`
ERC20_BEACON_ADDRESS_A=$BEACON_ADDRESS
ERC20_ADDRESS_A=$ERC20_ADDRESS
ERC20_BEACON_ADDRESS_B=`forge create $UPGRADE_BEACON_CONTRACT --broadcast --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY --constructor-args $DUMMY_ADDRESS $USER_ADDRESS | awk '/Deployed to:/ {print $3}'`
ERC20_ADDRESS_B=`forge create $PROXY_CONTRACT --broadcast --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY --constructor-args $ERC20_BEACON_ADDRESS_B 0x | awk '/Deployed to:/ {print $3}'`
```

### Deploy ERC7802 contracts

Replace the ERC20 contracts with contracts that:

* Support [ERC7802](https://eips.ethereum.org/EIPS/eip-7802) and [ERC165](https://eips.ethereum.org/EIPS/eip-165).
* Allow the bridge address to mint and burn tokens.
Normally this is `PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE`, but in our case it would be the bridge proxy address, which we'll store in `bridgeAddress`.
* Have the same storage layout as the ERC20 contracts they replace (in the case of chain A).

1. Create a file, `src/InteropToken.sol`.

```solidity file=<rootDir>/public/tutorials/InteropToken.sol hash=5e728534c265028c94d60dcb6550699d filename="src/InteropToken.sol"
```

2. This `src/InteropToken.sol` is used for contract upgrades when the ERC20 contracts are at the same address.
Here we need to edit it to allow our custom bridge to mint and burn tokens instead of the predeployed superchain token bridge.

* On lines 20 and 31 replace ~~`PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE`~~ with `bridgeAddress`.

```solidity
require(msg.sender == bridgeAddress, "Unauthorized");
```

* Add these lines anywhere in the contract:

```solidity
address public immutable bridgeAddress;

constructor(address bridgeAddress_) {
bridgeAddress = bridgeAddress_;
}
```

3. Deploy `InteropToken` on both chains, with the bridge address.

```sh
INTEROP_TOKEN_A=`forge create InteropToken --private-key $PRIVATE_KEY --broadcast --rpc-url $URL_CHAIN_A --constructor-args $BRIDGE_ADDRESS_A | awk '/Deployed to:/ {print $3}'`
INTEROP_TOKEN_B=`forge create InteropToken --private-key $PRIVATE_KEY --broadcast --rpc-url $URL_CHAIN_B --constructor-args $BRIDGE_ADDRESS_B | awk '/Deployed to:/ {print $3}'`
```

4. Update the proxies to the new implementations.

```sh
cast send $ERC20_BEACON_ADDRESS_A "upgradeTo(address)" $INTEROP_TOKEN_A --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A
cast send $ERC20_BEACON_ADDRESS_B "upgradeTo(address)" $INTEROP_TOKEN_B --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
```

### Deploy the actual bridge

1. Create a file, `src/CustomBridge.sol`.
This file is based on the standard `SuperchainERC20` [`SuperchainTokenBridge.sol`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol).

```solidity file=<rootDir>/public/tutorials/CustomBridge.sol hash=fc6cb08b40b7cf5bfb2b0c5793e4795e filename="src/CustomBridge.sol"
```

<details>
<summary>Explanation</summary>

These are the main differences between the generic bridge and our implementation.

```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L14-L18 hash=fd42623685f26046337ea6d105f27e4a
```

The configuration is [`immutable`](https://docs.soliditylang.org/en/latest/contracts.html#immutable).
We are deploying the contract behind a proxy, so if we need to change it we can deploy a different contract.

These parameters assume there are only two chains in the interop cluster.
If there are more, change the `There` variables to array.

```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L48-L65 hash=87edfdbb21fe11f573eff932d6f36d82
```

The constructor writes the configuration parameters.

```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L72-L75 hash=01ac207764b0d944ec4a005c7c3f01a8
```

We don't need to specify the token address, or the chain ID on the other side, because they are hardwired in this bridge.

```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L86 hash=d4ccbac2c6ce01bce65bc75242741f6e
```

Emit the same log entry that would be emitted by the standard bridge.

```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L100-L101 hash=142a487e877cc2cbeba8e07fce451c88
```

Make sure any relay requests come from the correct contract on the correct chain.
</details>

2. Get the chainID values.

```sh
CHAINID_A=`cast chain-id --rpc-url $URL_CHAIN_A`
CHAINID_B=`cast chain-id --rpc-url $URL_CHAIN_B`
```

3. Deploy the bridges with the correct configuration.

```sh
BRIDGE_IMPLEMENTATION_ADDRESS_A=`forge create CustomBridge --broadcast --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY --constructor-args $ERC20_ADDRESS_A $ERC20_ADDRESS_B $CHAINID_B $BRIDGE_ADDRESS_B | awk '/Deployed to:/ {print $3}'`
BRIDGE_IMPLEMENTATION_ADDRESS_B=`forge create CustomBridge --broadcast --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY --constructor-args $ERC20_ADDRESS_B $ERC20_ADDRESS_A $CHAINID_A $BRIDGE_ADDRESS_A | awk '/Deployed to:/ {print $3}'`
```

4. Inform the proxy beacons about the new addresses of the bridge implementation contracts.

```sh
cast send $BRIDGE_BEACON_ADDRESS_A "upgradeTo(address)" $BRIDGE_IMPLEMENTATION_ADDRESS_A --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A
cast send $BRIDGE_BEACON_ADDRESS_B "upgradeTo(address)" $BRIDGE_IMPLEMENTATION_ADDRESS_B --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
```

### Verification

1. See your balance on chain A.

```sh
cast call $ERC20_ADDRESS_A "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei
```

2. See your balance on chain B.

```sh
cast call $ERC20_ADDRESS_B "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei
```

3. Transfer 0.1 token.

```sh
AMOUNT=`echo 0.1 | cast to-wei`
cast send $BRIDGE_ADDRESS_A --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY "sendERC20(address,uint256)" $USER_ADDRESS $AMOUNT
```

4. See the new balances. The A chain should have 0.9 tokens, and the B chain should have 0.1 tokens.

```sh
cast call $ERC20_ADDRESS_A "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei
cast call $ERC20_ADDRESS_B "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei
```
</Steps>

## Next steps

* Deploy a [SuperchainERC20](/interop/tutorials/deploy-superchain-erc20) to the Superchain
* [Learn more about SuperchainERC20](/interop/superchain-erc20)
* Build a [revolutionary app](/app-developers/get-started) that uses multiple blockchains within the Superchain
Loading