|
| 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