This repository was archived by the owner on Apr 6, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 336
Tutorial: custom bridges (also how do have SuperchainERC20 equivalence without using the same addresses) #1574
Merged
Merged
Changes from 23 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
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
Large diffs are not rendered by default.
Oops, something went wrong.
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,107 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.25; | ||
|
|
||
| // Libraries | ||
| import { PredeployAddresses } from "interop-lib/src/libraries/PredeployAddresses.sol"; | ||
|
|
||
| // Interfaces | ||
| import { IERC7802, IERC165 } from "interop-lib/src/interfaces/IERC7802.sol"; | ||
| import { IL2ToL2CrossDomainMessenger } from "interop-lib/src/interfaces/IL2ToL2CrossDomainMessenger.sol"; | ||
|
|
||
| /// @custom:proxied true | ||
| /// @title CustomBridge | ||
| contract CustomBridge { | ||
| // Immutable configuration | ||
| address public immutable tokenAddressHere; | ||
| address public immutable tokenAddressThere; | ||
| uint256 public immutable chainIdThere; | ||
| address public immutable bridgeAddressThere; | ||
|
|
||
| error ZeroAddress(); | ||
| error Unauthorized(); | ||
|
|
||
| /// @notice Thrown when attempting to relay a message and the cross domain message sender is not the | ||
| /// SuperchainTokenBridge. | ||
| error InvalidCrossDomainSender(); | ||
|
|
||
| /// @notice Emitted when tokens are sent from one chain to another. | ||
| /// @param token Address of the token sent. | ||
| /// @param from Address of the sender. | ||
| /// @param to Address of the recipient. | ||
| /// @param amount Number of tokens sent. | ||
| /// @param destination Chain ID of the destination chain. | ||
| event SendERC20( | ||
| address indexed token, address indexed from, address indexed to, uint256 amount, uint256 destination | ||
| ); | ||
|
|
||
| /// @notice Emitted whenever tokens are successfully relayed on this chain. | ||
| /// @param token Address of the token relayed. | ||
| /// @param from Address of the msg.sender of sendERC20 on the source chain. | ||
| /// @param to Address of the recipient. | ||
| /// @param amount Amount of tokens relayed. | ||
| /// @param source Chain ID of the source chain. | ||
| event RelayERC20(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 source); | ||
|
|
||
| /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. | ||
| address internal constant MESSENGER = PredeployAddresses.L2_TO_L2_CROSS_DOMAIN_MESSENGER; | ||
|
|
||
| // Setup the configuration | ||
| constructor( | ||
| address tokenAddressHere_, | ||
| address tokenAddressThere_, | ||
| uint256 chainIdThere_, | ||
| address bridgeAddressThere_ | ||
| ) { | ||
| if ( | ||
| tokenAddressHere_ == address(0) || | ||
| tokenAddressThere_ == address(0) || | ||
| bridgeAddressThere_ == address(0) | ||
| ) revert ZeroAddress(); | ||
|
|
||
| tokenAddressHere = tokenAddressHere_; | ||
| tokenAddressThere = tokenAddressThere_; | ||
| chainIdThere = chainIdThere_; | ||
| bridgeAddressThere = bridgeAddressThere_; | ||
| } | ||
|
|
||
| /// @notice Sends tokens to a target address on another chain. | ||
| /// @dev Tokens are burned on the source chain. | ||
| /// @param _to Address to send tokens to. | ||
| /// @param _amount Amount of tokens to send. | ||
| /// @return msgHash_ Hash of the message sent. | ||
| function sendERC20( | ||
| address _to, | ||
| uint256 _amount | ||
| ) | ||
| external | ||
| returns (bytes32 msgHash_) | ||
| { | ||
| if (_to == address(0)) revert ZeroAddress(); | ||
|
|
||
| IERC7802(tokenAddressHere).crosschainBurn(msg.sender, _amount); | ||
|
|
||
| bytes memory message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount)); | ||
| msgHash_ = IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(chainIdThere, bridgeAddressThere, message); | ||
|
|
||
| emit SendERC20(tokenAddressHere, msg.sender, _to, _amount, chainIdThere); | ||
| } | ||
|
|
||
| /// @notice Relays tokens received from another chain. | ||
| /// @dev Tokens are minted on the destination chain. | ||
| /// @param _from Address of the msg.sender of sendERC20 on the source chain. | ||
| /// @param _to Address to relay tokens to. | ||
| /// @param _amount Amount of tokens to relay. | ||
| function relayERC20(address _from, address _to, uint256 _amount) external { | ||
| if (msg.sender != MESSENGER) revert Unauthorized(); | ||
|
|
||
| (address crossDomainMessageSender, uint256 source) = | ||
| IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageContext(); | ||
|
|
||
| if (crossDomainMessageSender != bridgeAddressThere) revert InvalidCrossDomainSender(); | ||
| if (source != chainIdThere) revert InvalidCrossDomainSender(); | ||
|
|
||
| IERC7802(tokenAddressHere).crosschainMint(_to, _amount); | ||
|
|
||
| emit RelayERC20(tokenAddressHere, _from, _to, _amount, chainIdThere); | ||
| } | ||
| } | ||
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,44 @@ | ||
| pragma solidity ^0.8.28; | ||
|
|
||
| import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
| import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | ||
| import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
| import {IERC7802, IERC165} from "lib/interop-lib/src/interfaces/IERC7802.sol"; | ||
| import {PredeployAddresses} from "lib/interop-lib/src/libraries/PredeployAddresses.sol"; | ||
|
|
||
| contract InteropToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, IERC7802 { | ||
| function initialize(string memory name, string memory symbol, uint256 initialSupply) public initializer { | ||
| __ERC20_init(name, symbol); | ||
| __Ownable_init(msg.sender); | ||
| _mint(msg.sender, initialSupply); | ||
| } | ||
|
|
||
| /// @notice Allows the SuperchainTokenBridge to mint tokens. | ||
| /// @param _to Address to mint tokens to. | ||
| /// @param _amount Amount of tokens to mint. | ||
| function crosschainMint(address _to, uint256 _amount) external { | ||
| require(msg.sender == PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE, "Unauthorized"); | ||
|
|
||
| _mint(_to, _amount); | ||
|
|
||
| emit CrosschainMint(_to, _amount, msg.sender); | ||
| } | ||
|
|
||
| /// @notice Allows the SuperchainTokenBridge to burn tokens. | ||
| /// @param _from Address to burn tokens from. | ||
| /// @param _amount Amount of tokens to burn. | ||
| function crosschainBurn(address _from, uint256 _amount) external { | ||
| require(msg.sender == PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE, "Unauthorized"); | ||
|
|
||
| _burn(_from, _amount); | ||
|
|
||
| emit CrosschainBurn(_from, _amount, msg.sender); | ||
| } | ||
|
|
||
| /// @inheritdoc IERC165 | ||
| function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) { | ||
| return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId | ||
| || _interfaceId == type(IERC165).interfaceId; | ||
| } | ||
| } | ||
|
|
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,77 @@ | ||
| #! /bin/sh | ||
|
|
||
| rm -rf upgrade-erc20 || { echo "Failed to remove directory"; exit 1; } | ||
| mkdir upgrade-erc20 || { echo "Failed to create directory"; exit 1; } | ||
| cd upgrade-erc20 || { echo "Failed to change directory"; exit 1; } | ||
|
|
||
| if [ -z $1 ] | ||
| then | ||
| echo Supersim | ||
| # This is a well-known development private key – never use in production | ||
| PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | ||
| URL_CHAIN_A=http://localhost:9545 | ||
| URL_CHAIN_B=http://localhost:9546 | ||
| else | ||
| echo Devnet | ||
| PRIVATE_KEY=$1 | ||
| URL_CHAIN_A=https://interop-alpha-0.optimism.io | ||
| URL_CHAIN_B=https://interop-alpha-1.optimism.io | ||
| fi | ||
|
qbzzt marked this conversation as resolved.
qbzzt marked this conversation as resolved.
|
||
|
|
||
| USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY` | ||
|
|
||
| forge init | ||
| forge install OpenZeppelin/openzeppelin-contracts-upgradeable | ||
|
|
||
| cat > script/LabSetup.s.sol <<EOF | ||
| // SPDX-License-Identifier: UNLICENSED | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {Script, console} from "forge-std/Script.sol"; | ||
| import {UpgradeableBeacon} from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; | ||
| import {BeaconProxy} from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/BeaconProxy.sol"; | ||
|
|
||
| import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
| import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | ||
| import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
|
|
||
| contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable { | ||
| function initialize(string memory name, string memory symbol, uint256 initialSupply) public initializer { | ||
| __ERC20_init(name, symbol); | ||
| __Ownable_init(msg.sender); | ||
| _mint(msg.sender, initialSupply); | ||
| } | ||
| } | ||
|
|
||
| contract LabSetup is Script { | ||
| function setUp() public {} | ||
|
|
||
| function run() public { | ||
| vm.startBroadcast(); | ||
|
|
||
| MyToken token = new MyToken(); | ||
| console.log("Token address:", address(token)); | ||
| console.log("msg.sender:", msg.sender); | ||
|
|
||
| UpgradeableBeacon beacon = new UpgradeableBeacon(address(token), msg.sender); | ||
| console.log("UpgradeableBeacon:", address(beacon)); | ||
|
|
||
| BeaconProxy proxy = new BeaconProxy(address(beacon), | ||
| abi.encodeCall(MyToken.initialize, ("Test", "TST", | ||
| (block.chainid == 901) || (block.chainid == 420120000) ? 10**18 : 0)) | ||
| ); | ||
| console.log("Proxy:", address(proxy)); | ||
|
|
||
| vm.stopBroadcast(); | ||
| } | ||
| } | ||
| EOF | ||
|
|
||
| forge script script/LabSetup.s.sol --rpc-url $URL_CHAIN_A --broadcast --private-key $PRIVATE_KEY --tc LabSetup | tee setup_output | ||
|
|
||
| BEACON_ADDRESS=`cat setup_output | awk '/Beacon:/ {print $2}'` | ||
| ERC20_ADDRESS=`cat setup_output | awk '/Proxy:/ {print $2}'` | ||
|
|
||
| echo Run these commands to store the configuration: | ||
| echo BEACON_ADDRESS=$BEACON_ADDRESS | ||
| echo export ERC20_ADDRESS=$ERC20_ADDRESS | ||
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 |
|---|---|---|
|
|
@@ -95,8 +95,6 @@ Devnet | |
| devnet | ||
| Devnets | ||
| devnets | ||
| Devs | ||
|
|
||
| direnv | ||
| DISABLETXPOOLGOSSIP | ||
| disabletxpoolgossip | ||
|
|
||
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.