-
Notifications
You must be signed in to change notification settings - Fork 268
Tutorial: custom bridges (also how do have SuperchainERC20 equivalence without using the same addresses) #1574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
071da34
WIP
qbzzt f9c3040
WIP
qbzzt 75b3ce2
Working 1st draft
qbzzt b3535ad
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt 83c8a2a
Full version
qbzzt 29bf26a
@zainbacchus comments
qbzzt bb9a679
Merge branch 'main' into 250412-custom-bridge
qbzzt 8e8648e
Update custom-bridge.mdx
qbzzt 200de29
Merge branch 'main' into 250412-custom-bridge
qbzzt 28b4496
Maybe now it'll build
qbzzt fec0467
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt babb2b6
Maybe now?
qbzzt 98340fe
Fix nav
bradleycamacho 8d6f41f
Merge branch '250412-custom-bridge' of https://github.com/qbzzt/optim…
bradleycamacho 2fcc9d7
Uppercase Supersim
qbzzt 4fd1d18
Apply suggestions from code review
qbzzt bb44dfd
Fixed hashed and code lines
qbzzt 63cb35f
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt 6e20053
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt e578ccd
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt b9b74d9
Update pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bri…
qbzzt 2159558
lint
qbzzt 3efad2f
lint
qbzzt f33ceff
lint, yet another issue
qbzzt e633a05
Merge branch 'main' into 250412-custom-bridge
krofax a39d55f
Merge branch 'main' into 250412-custom-bridge
qbzzt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
pages/interop/tutorials/upgrade-to-superchain-erc20/_meta.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"custom-bridge": "Building a custom bridge" | ||
} |
348 changes: 348 additions & 0 deletions
348
pages/interop/tutorials/upgrade-to-superchain-erc20/custom-bridge.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
*/} | ||
qbzzt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## 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. | ||
|
||
krofax marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.