Skip to content

Commit d71d324

Browse files
authored
Merge pull request #1574 from qbzzt/250412-custom-bridge
Tutorial: custom bridges (also how do have SuperchainERC20 equivalence without using the same addresses)
2 parents 41addef + a39d55f commit d71d324

File tree

7 files changed

+583
-2
lines changed

7 files changed

+583
-2
lines changed

pages/interop/tutorials/_meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
"relay-messages-viem": "Relaying interop messages using `viem`",
99
"contract-calls": "Making crosschain contract calls (ping pong)",
1010
"event-reads": "Making crosschain event reads (tic-tac-toe)",
11-
"event-contests": "Deploying crosschain event composability (contests)"
11+
"event-contests": "Deploying crosschain event composability (contests)",
12+
"upgrade-to-superchain-erc20": "Upgrade to SuperchainERC20"
1213
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"custom-bridge": "Building a custom bridge"
3+
}
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
---
2+
title: Building a custom bridge
3+
lang: en-US
4+
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.
5+
topic: Interoperability
6+
personas: [Developer]
7+
categories: [Tutorial, Interop]
8+
content_type: article
9+
---
10+
11+
import { Steps, Callout, Tabs } from 'nextra/components'
12+
13+
# Building a custom bridge
14+
15+
## Overview
16+
17+
Sometimes the address of an ERC20 contract is not available on a different chain.
18+
This means that the [SuperchainTokenBridge](/interop/superchain-erc20) is not an option.
19+
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.
20+
21+
<details>
22+
<summary>About this tutorial</summary>
23+
24+
**What you'll learn**
25+
26+
* How to use [interop message passing](/interop/tutorials/message-passing) to create a custom bridge.
27+
28+
**Prerequisite knowledge**
29+
30+
* How to [deploy SuperchainERC20 tokens with custom code](/interop/tutorials/custom-superchain-erc20).
31+
* How to [transfer interop messages](/interop/tutorials/message-passing).
32+
</details>
33+
34+
<Callout type="warning">
35+
The code on the documentation site is sample code, *not* production code.
36+
This means that we ran it, and it works as advertised.
37+
However, it did not pass through the rigorous audit process that most Optimism code undergoes.
38+
You're welcome to use it, but if you need it for production purposes you should get it audited first.
39+
</Callout>
40+
41+
{/*
42+
43+
I put this warning here, when we don't have it on most pages, because this tutorial
44+
has code that is a lot more likely to be used in production. It doesn't just
45+
show what is possible, it does the exact job needed.
46+
47+
*/}
48+
49+
## How beacon proxies work
50+
51+
```mermaid
52+
sequenceDiagram
53+
Actor User
54+
User->>BeaconProxy: transfer(<address>, <amount>)
55+
BeaconProxy->>UpgradeableBeacon: What is the implementation address?
56+
UpgradeableBeacon->>BeaconProxy: It is 0xBAD0...60A7
57+
BeaconProxy->>0xBAD0...60A7: transfer(<address>, <amount>)
58+
```
59+
60+
A [beacon proxy](https://docs.openzeppelin.com/contracts/3.x/api/proxy#BeaconProxy) uses two contracts.
61+
The [`UpgradeableBeacon`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/UpgradeableBeacon.sol) contract holds the address of the implementation contract.
62+
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.
63+
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.
64+
65+
```mermaid
66+
sequenceDiagram
67+
Actor User
68+
Actor Owner
69+
Participant BeaconProxy
70+
Participant 0x600D...60A7
71+
Owner->>UpgradeableBeacon: Your new implementation address is 0x600D...60A7
72+
User->>BeaconProxy: transfer(<address>, <amount>)
73+
BeaconProxy->>UpgradeableBeacon: What is the implementation address?
74+
UpgradeableBeacon->>BeaconProxy: It is 0x600D...60A7
75+
BeaconProxy->>0x600D...60A7: transfer(<address>, <amount>)
76+
```
77+
78+
To upgrade the contract, an authorized address (typically the `Owner`) calls `UpgradeableBeacon` directly to specify the new implementation contract address.
79+
After that happens, all new calls are sent to the new implementation.
80+
81+
## Instructions
82+
83+
Some steps depend on whether you want to deploy on [Supersim](/interop/tools/supersim) or on the [development network](/interop/tools/devnet).
84+
85+
<Steps>
86+
### Install and run Supersim
87+
88+
If you are going to use Supersim, [follow these instructions](/app-developers/tutorials/supersim/getting-started/installation) to install and run Supersim.
89+
90+
<Callout>
91+
Make sure to run Supersim with autorelay on.
92+
93+
```sh
94+
./supersim --interop.autorelay true
95+
```
96+
</Callout>
97+
98+
### Set up the ERC20 token on chain A
99+
100+
Download and run the setup script.
101+
102+
```sh
103+
curl https://docs.optimism.io/tutorials/setup-for-erc20-upgrade.sh > setup-for-erc20-upgrade.sh
104+
chmod +x setup-for-erc20-upgrade.sh
105+
./setup-for-erc20-upgrade.sh
106+
```
107+
108+
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.
109+
110+
```sh
111+
./setup-for-erc20-upgrade.sh <private key>
112+
```
113+
114+
### Store the addresses
115+
116+
Execute the bottom two lines of the setup script output to store the ERC20 address and the address of the beacon contract.
117+
118+
```sh
119+
BEACON_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
120+
export ERC20_ADDRESS=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
121+
```
122+
123+
### Specify environment variables
124+
125+
Specify these variables, which we use later:
126+
127+
<Tabs items={['Supersim', 'Devnets']}>
128+
<Tabs.Tab>
129+
Set these parameters for Supersim.
130+
131+
```sh
132+
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
133+
USER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
134+
URL_CHAIN_A=http://127.0.0.1:9545
135+
URL_CHAIN_B=http://127.0.0.1:9546
136+
```
137+
</Tabs.Tab>
138+
139+
<Tabs.Tab>
140+
For Devnet, specify in `PRIVATE_KEY` the private key you used for the setup script and then these parameters.
141+
142+
```sh
143+
USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY`
144+
URL_CHAIN_A=https://interop-alpha-0.optimism.io
145+
URL_CHAIN_B=https://interop-alpha-1.optimism.io
146+
```
147+
</Tabs.Tab>
148+
</Tabs>
149+
150+
### Advance the user's nonce on chain B
151+
152+
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.
153+
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.
154+
155+
```sh
156+
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
157+
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
158+
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
159+
cast send $USER_ADDRESS --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
160+
echo -n Nonce on chain A:
161+
cast nonce $USER_ADDRESS --rpc-url $URL_CHAIN_A
162+
echo -n Nonce on chain B:
163+
cast nonce $USER_ADDRESS --rpc-url $URL_CHAIN_B
164+
```
165+
166+
### Create a Foundry project
167+
168+
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.
169+
170+
```sh
171+
mkdir custom-bridge
172+
cd custom-bridge
173+
forge init
174+
forge install OpenZeppelin/openzeppelin-contracts
175+
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
176+
forge install ethereum-optimism/interop-lib
177+
```
178+
179+
### Deploy proxies
180+
181+
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.
182+
You already have one contract—the ERC20 on chain A—but need to create the other three.
183+
184+
There's an interesting [chicken-and-egg](https://en.wikipedia.org/wiki/Chicken_or_the_egg) issue here.
185+
To create a proxy, we need the address of the implementation contract, the one with the actual code.
186+
However, the bridge and ERC20 code needs to have the proxy addresses.
187+
One possible solution is to choose a pre-existing contract, and use that as the implementation contract until we can upgrade.
188+
Every OP Stack chain has [predeploys](https://specs.optimism.io/protocol/predeploys.html) we can use for this purpose.
189+
190+
```sh
191+
DUMMY_ADDRESS=0x4200000000000000000000000000000000000000
192+
UPGRADE_BEACON_CONTRACT=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol:UpgradeableBeacon
193+
PROXY_CONTRACT=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/BeaconProxy.sol:BeaconProxy
194+
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}'`
195+
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}'`
196+
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}'`
197+
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}'`
198+
ERC20_BEACON_ADDRESS_A=$BEACON_ADDRESS
199+
ERC20_ADDRESS_A=$ERC20_ADDRESS
200+
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}'`
201+
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}'`
202+
```
203+
204+
### Deploy ERC7802 contracts
205+
206+
Replace the ERC20 contracts with contracts that:
207+
208+
* Support [ERC7802](https://eips.ethereum.org/EIPS/eip-7802) and [ERC165](https://eips.ethereum.org/EIPS/eip-165).
209+
* Allow the bridge address to mint and burn tokens.
210+
Normally this is `PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE`, but in our case it would be the bridge proxy address, which we'll store in `bridgeAddress`.
211+
* Have the same storage layout as the ERC20 contracts they replace (in the case of chain A).
212+
213+
1. Create a file, `src/InteropToken.sol`.
214+
215+
```solidity file=<rootDir>/public/tutorials/InteropToken.sol hash=5e728534c265028c94d60dcb6550699d filename="src/InteropToken.sol"
216+
```
217+
218+
2. This `src/InteropToken.sol` is used for contract upgrades when the ERC20 contracts are at the same address.
219+
Here we need to edit it to allow our custom bridge to mint and burn tokens instead of the predeployed superchain token bridge.
220+
221+
* On lines 20 and 31 replace ~~`PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE`~~ with `bridgeAddress`.
222+
223+
```solidity
224+
require(msg.sender == bridgeAddress, "Unauthorized");
225+
```
226+
227+
* Add these lines anywhere in the contract:
228+
229+
```solidity
230+
address public immutable bridgeAddress;
231+
232+
constructor(address bridgeAddress_) {
233+
bridgeAddress = bridgeAddress_;
234+
}
235+
```
236+
237+
3. Deploy `InteropToken` on both chains, with the bridge address.
238+
239+
```sh
240+
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}'`
241+
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}'`
242+
```
243+
244+
4. Update the proxies to the new implementations.
245+
246+
```sh
247+
cast send $ERC20_BEACON_ADDRESS_A "upgradeTo(address)" $INTEROP_TOKEN_A --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A
248+
cast send $ERC20_BEACON_ADDRESS_B "upgradeTo(address)" $INTEROP_TOKEN_B --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
249+
```
250+
251+
### Deploy the actual bridge
252+
253+
1. Create a file, `src/CustomBridge.sol`.
254+
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).
255+
256+
```solidity file=<rootDir>/public/tutorials/CustomBridge.sol hash=fc6cb08b40b7cf5bfb2b0c5793e4795e filename="src/CustomBridge.sol"
257+
```
258+
259+
<details>
260+
<summary>Explanation</summary>
261+
262+
These are the main differences between the generic bridge and our implementation.
263+
264+
```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L14-L18 hash=fd42623685f26046337ea6d105f27e4a
265+
```
266+
267+
The configuration is [`immutable`](https://docs.soliditylang.org/en/latest/contracts.html#immutable).
268+
We are deploying the contract behind a proxy, so if we need to change it we can deploy a different contract.
269+
270+
These parameters assume there are only two chains in the interop cluster.
271+
If there are more, change the `There` variables to array.
272+
273+
```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L48-L65 hash=87edfdbb21fe11f573eff932d6f36d82
274+
```
275+
276+
The constructor writes the configuration parameters.
277+
278+
```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L72-L75 hash=01ac207764b0d944ec4a005c7c3f01a8
279+
```
280+
281+
We don't need to specify the token address, or the chain ID on the other side, because they are hardwired in this bridge.
282+
283+
```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L86 hash=d4ccbac2c6ce01bce65bc75242741f6e
284+
```
285+
286+
Emit the same log entry that would be emitted by the standard bridge.
287+
288+
```solidity file=<rootDir>/public/tutorials/CustomBridge.sol#L100-L101 hash=142a487e877cc2cbeba8e07fce451c88
289+
```
290+
291+
Make sure any relay requests come from the correct contract on the correct chain.
292+
</details>
293+
294+
2. Get the chainID values.
295+
296+
```sh
297+
CHAINID_A=`cast chain-id --rpc-url $URL_CHAIN_A`
298+
CHAINID_B=`cast chain-id --rpc-url $URL_CHAIN_B`
299+
```
300+
301+
3. Deploy the bridges with the correct configuration.
302+
303+
```sh
304+
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}'`
305+
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}'`
306+
```
307+
308+
4. Inform the proxy beacons about the new addresses of the bridge implementation contracts.
309+
310+
```sh
311+
cast send $BRIDGE_BEACON_ADDRESS_A "upgradeTo(address)" $BRIDGE_IMPLEMENTATION_ADDRESS_A --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A
312+
cast send $BRIDGE_BEACON_ADDRESS_B "upgradeTo(address)" $BRIDGE_IMPLEMENTATION_ADDRESS_B --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B
313+
```
314+
315+
### Verification
316+
317+
1. See your balance on chain A.
318+
319+
```sh
320+
cast call $ERC20_ADDRESS_A "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei
321+
```
322+
323+
2. See your balance on chain B.
324+
325+
```sh
326+
cast call $ERC20_ADDRESS_B "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei
327+
```
328+
329+
3. Transfer 0.1 token.
330+
331+
```sh
332+
AMOUNT=`echo 0.1 | cast to-wei`
333+
cast send $BRIDGE_ADDRESS_A --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY "sendERC20(address,uint256)" $USER_ADDRESS $AMOUNT
334+
```
335+
336+
4. See the new balances. The A chain should have 0.9 tokens, and the B chain should have 0.1 tokens.
337+
338+
```sh
339+
cast call $ERC20_ADDRESS_A "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei
340+
cast call $ERC20_ADDRESS_B "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei
341+
```
342+
</Steps>
343+
344+
## Next steps
345+
346+
* Deploy a [SuperchainERC20](/interop/tutorials/deploy-superchain-erc20) to the Superchain
347+
* [Learn more about SuperchainERC20](/interop/superchain-erc20)
348+
* Build a [revolutionary app](/app-developers/get-started) that uses multiple blockchains within the Superchain

0 commit comments

Comments
 (0)